aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/ark.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-09 16:24:58 -0700
committerAdam Soltys <[email protected]>2026-10-09 16:24:58 -0700
commit20369cae83fe02968ac4318f3b8f4e9dcbbaa1cf (patch)
tree569afd7b137500087f2517fd04b545f4639a967b /src/features/ark.js
parent52612bc8bc4c9ea18b0520fc23c98094f304cc05 (diff)
downloadcoinosv3-20369cae83fe02968ac4318f3b8f4e9dcbbaa1cf.tar.gz
coinosv3-20369cae83fe02968ac4318f3b8f4e9dcbbaa1cf.zip
Spending: a Lightning receive is one row. The ASP settles it as an arkoor the wallet claims — recorded as ln-receive, without its coin's id — and the same coin can reach the mailbox too, which (seeing no movement for that coin) recorded a plain receive as well: two "Received 1,005" rows and a "Payment received! 2,010" (The Daniel). The claim now stamps its coins' ids on the ln-receive, the mailbox backfill counts them, and lnFoldedIds leaves a receive of a claimed coin out of history, the celebration, the arkMovements hook and the LN tagging — derived at read time, never deleted, so a synced copy can't bring the twin back. Claims recorded before the stamp match by amount within ten minutes. tools/ln-receive-twin-test.js
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3
Diffstat (limited to 'src/features/ark.js')
-rw-r--r--src/features/ark.js9
1 files changed, 6 insertions, 3 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index 2aef8c5..13481da 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -6,7 +6,7 @@ import * as btc from '@scure/btc-signer';
import { HDKey } from '@scure/bip32';
import { hex, base32nopad, bech32 } from '@scure/base';
import { sha256 } from '@noble/hashes/sha256';
-import { ArkManager, remoteRenewalsInFlight } from '../ark/manager.js';
+import { ArkManager, remoteRenewalsInFlight, lnFoldedIds } from '../ark/manager.js';
export { remoteRenewalsInFlight };
// Coins this device may offer: spendable, and not held by a renewal another
@@ -727,8 +727,9 @@ export function arkFeature(ctx) {
try {
const receipts = await Promise.resolve(ctx.hook('namesLnReceipts')).catch(() => null);
if (!receipts || !receipts.length || ark !== mgr || !mgr.state) return;
+ const twins = lnFoldedIds(mgr.state.movements);
const moves = (mgr.state.movements || [])
- .filter((m) => m.type === 'receive' && m.status === 'complete');
+ .filter((m) => m.type === 'receive' && m.status === 'complete' && !twins.has(m.id));
const used = new Set(moves.filter((m) => m.lnHash).map((m) => m.lnHash));
let changed = false;
for (const r of receipts) {
@@ -3796,7 +3797,7 @@ export function arkFeature(ctx) {
if (a) await Promise.race([mgr.driveLn(a.id).catch(() => {}), new Promise((r) => setTimeout(r, 8000))]);
return mgr.lnLookup(q);
},
- arkMovements() { const s = arkStateNow(); return s ? (s.movements || []) : []; },
+ arkMovements() { const s = arkStateNow(); if (!s) return []; const twins = lnFoldedIds(s.movements); return (s.movements || []).filter((m) => !twins.has(m.id)); },
// The wallet's reusable ark receive address (BIP-353 names publish it).
async arkStaticAddress() {
const mgr = await connectArk();
@@ -3960,7 +3961,9 @@ export function arkFeature(ctx) {
// participation), and only ones that COST something get a row — a free
// renewal is net zero, invisible money movement, noise.
const seenPay = new Set();
+ const lnTwins = lnFoldedIds(s.movements); // a claimed LN receive's coin, told again by the mailbox
const moves = (s.movements || [])
+ .filter((m) => !lnTwins.has(m.id))
.filter((m) => ['receive', 'send', 'board', 'offboard', 'exit', 'ln-send', 'ln-receive', 'refresh', 'reconcile'].includes(m.type)
&& (m.status === 'complete' || (m.type === 'send' && m.status === 'failed')))
.filter((m) => m.type !== 'refresh' || m.feeSat > 0 || m.manual) // a renewal you asked for is worth a row even when free