aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-22 11:24:34 -0700
committerAdam Soltys <[email protected]>2026-09-22 11:24:34 -0700
commit229746922fbd566ef9014845c2111c5044ac91fa (patch)
tree074d800cef61ce3c75bdfd7731828d8ad2f0280d /src
parentff0038baed91f79722c3a3ac71651cdd1fc145e8 (diff)
downloadcoinosv3-229746922fbd566ef9014845c2111c5044ac91fa.tar.gz
coinosv3-229746922fbd566ef9014845c2111c5044ac91fa.zip
Ark: receives waiting with another server are claimed; notifications belong to the identity.
Once per session the wallet visits every other Ark server it holds state with — its own manager, a receive rescan, a sync to claim — and says what arrived and where; the coins wait there until a switch back. The stored notification list remembers which identity fetched it, so another identity on the same wallet starts empty instead of reading the previous one's. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Tmfg8k5f3TbNJS34qiqHAp
Diffstat (limited to 'src')
-rw-r--r--src/features/ark.js39
-rw-r--r--src/features/messages.js9
-rw-r--r--src/i18n.js1
3 files changed, 46 insertions, 3 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 2b81a1e..ab7f28d 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -417,6 +417,7 @@ export function arkFeature(ctx) {
// the next connect's first tick re-decides for the wallet now open.
arkRenewWarn = null;
arkAutoRefreshAt = 0;
+ rescuedServers = false;
}
function arkAvailable() {
@@ -443,7 +444,7 @@ export function arkFeature(ctx) {
store.open().then(() => { ctx.forgetAmountAnim && ctx.forgetAmountAnim(); render(); initArk(); }).catch(() => {});
return;
}
- if (arkAvailable() && arkWanted()) { lastAutoInit = Date.now(); connectArk().catch(() => {}); }
+ if (arkAvailable() && arkWanted()) { lastAutoInit = Date.now(); connectArk().then(() => rescueOtherServers()).catch(() => {}); }
}
// Automatic re-inits (a synced snapshot arriving) must not restart a
@@ -452,6 +453,42 @@ export function arkFeature(ctx) {
// card's "connecting" indicator flickers on every snapshot. User-driven
// calls to initArk() stay immediate.
let lastAutoInit = 0;
+ // A payment can land with a server this wallet is NOT connected to: an
+ // invoice minted for its coinos name by the background answerer before a
+ // switch, or by another device still on the old server. Once per session,
+ // every other server this wallet holds state with gets a short visit — its
+ // own manager, a receive rescan, a sync to claim — and a toast says what
+ // arrived and where. The coins stay with that server until a switch back.
+ let rescuedServers = false;
+ async function rescueOtherServers() {
+ if (rescuedServers || wallet.watchOnly) return;
+ rescuedServers = true;
+ const cur = getArkConfig();
+ if (!cur) return;
+ for (const p of arkPresets(getNetwork())) {
+ if (!p.ark || p.ark === cur.ark || p.id === 'custom' || p.id === 'off') continue;
+ if (!wallet.loadArkState(p.ark)) continue; // never used that server
+ try {
+ const mgr = new ArkManager({
+ account: wallet.account(),
+ storage: {
+ load: () => wallet.loadArkState(p.ark),
+ adopt: (serverPubkey) => wallet.adoptArkState(serverPubkey, p.ark),
+ save: (s) => { wallet.saveArkState(mergeArkStates(s, wallet.loadArkState(p.ark)), p.ark); },
+ },
+ arkUrl: p.ark, esploraUrl: p.esplora, network: getNetwork(),
+ });
+ await mgr.init();
+ const done = () => (mgr.state.movements || []).filter((m) => m.type === 'ln-receive' && m.status === 'complete');
+ const before = done().length;
+ await mgr.sync(); // rescans receives and drives the claims
+ await new Promise((r) => setTimeout(r, 2500));
+ await mgr.sync(); // a claim takes more than one step
+ const got = done().slice(before).reduce((n, m) => n + (m.amountSat || 0), 0);
+ if (got > 0) toast(t('arkOtherServerReceipt', { amount: fmtSats(got), server: p.label.replace(/ \(.*\)$/, '') }), 9000);
+ } catch (e) { console.warn('ark: rescue on ' + p.ark + ' failed', e); }
+ }
+ }
function maybeInitArk() {
if (ark || arkConnectPromise) return;
if (Date.now() - lastAutoInit < 20000) return;
diff --git a/src/features/messages.js b/src/features/messages.js
index b97ff8d..e5bc300 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -7063,9 +7063,14 @@ export function messagesFeature(ctx) {
let notif = null; // { status, items }
let notifAt = 0, notifUnsub = null;
const notifSeen = () => st().notifSeen || 0;
+ // Notifications are about the IDENTITY, not the wallet: the stored list
+ // remembers whose it is, and another identity on the same wallet starts
+ // empty instead of reading the previous one's.
+ const identityPk = () => (hook('nostrLoginIdentity') || {}).pubkey || (wallet.nostr && wallet.nostr.pk) || null;
function notifNow() {
if (!notif) {
- const stored = st().notifs || [];
+ const s = st();
+ const stored = (!s.notifsPk || s.notifsPk === identityPk()) ? (s.notifs || []) : [];
notif = { status: stored.length ? 'ready' : 'loading', items: stored };
refreshNotifs();
}
@@ -7116,7 +7121,7 @@ export function messagesFeature(ctx) {
c.items = kept.slice(0, NOTIF_KEEP);
// the rows already say who; a reply row also wants its reader-facing text,
// which it carries — the events themselves are not kept
- const s2 = st(); s2.notifs = c.items; save(s2);
+ const s2 = st(); s2.notifs = c.items; s2.notifsPk = identityPk(); save(s2);
// a reply is a note we can open straight away; keep it in hand
for (const ev of evs) if (ev.kind === 1) notifNotes.set(ev.id, ev);
return true;
diff --git a/src/i18n.js b/src/i18n.js
index 74219ac..d1a6962 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -860,6 +860,7 @@ const STR = {
arkCmpMin: 'min {n} sats',
arkCmpNote: 'Fees graded by expiry cost more the earlier a coin is renewed or exited. Figures come from each server’s own published info, read just now. Either server pays Lightning invoices and takes Lightning payments to your coinos name.',
arkServerSwitched: 'Ark server switched',
+ arkOtherServerReceipt: '{amount} sats arrived with {server}. Switch to that server in Settings → Advanced to spend them.',
arkForeignBip21: 'Their Ark address is on a different Ark server — paying the on-chain address instead.',
arkForeignServer: 'That Ark address belongs to a different Ark server, so it can’t be paid from here directly. Ask for a Lightning invoice, or pay their coinos name or npub — those fall back to Lightning by themselves.',
arkLnFromSavingsTitle: 'Lightning pays from Spending',