aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-07-06 10:44:35 -0700
committerAdam Soltys <[email protected]>2026-07-06 10:44:35 -0700
commitef1a901314cb667bc8a7c7ff5f080550edef2815 (patch)
tree5119e7034771a9a5a6ad7a39e21010690dedbea5 /src
parentf8708f5472b7fa9ee6f0a248a0d6461ff259cb94 (diff)
downloadcoinosv3-ef1a901314cb667bc8a7c7ff5f080550edef2815.tar.gz
coinosv3-ef1a901314cb667bc8a7c7ff5f080550edef2815.zip
Slim ark state for sync so it fits the relay's 64KB event cap
Cross-device ark sync silently failed: the full snapshot (deep-genesis vtxo bytes + done-action signing hex) was ~153KB and relay.coinos.io rejects events over 64KB, so no publish ever landed and other devices never saw the balance. The synced ark state is now slimmed — spent vtxos drop their signed bytes (kept only for spendable/pending, which a device might spend/exit), done board/offboard/exit actions keep just the fields history labels need (hex stripped), in-flight actions and movements beyond the last 300 are dropped. Local storage keeps the full state; only the published copy is slim. ~150KB -> well under 64KB. Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/features/ark.js34
-rw-r--r--src/features/sync.js3
-rw-r--r--src/nostr.js5
3 files changed, 31 insertions, 11 deletions
diff --git a/src/features/ark.js b/src/features/ark.js
index d0f17f2..f22d7be 100644
--- a/src/features/ark.js
+++ b/src/features/ark.js
@@ -37,10 +37,33 @@ export function installArkWallet(wallet) {
});
}
+// A slim projection of ark state for the sync snapshot — the sync relay caps
+// events at 64 KB and the full state (deep-genesis vtxo bytes + done-action
+// hex) blew past it, so publishes silently failed and nothing synced. Another
+// device needs, per vtxo: id/amount/state (+ bytes ONLY for spendable/pending,
+// which it might spend/exit); the movement log (capped) for ark history; and
+// done board/offboard/exit actions (stripped of hex) so on-chain rows still
+// label. In-flight actions and their signing material stay device-local.
+export function slimArkForSync(s) {
+ if (!s) return null;
+ const HEAVY = ['bytes', 'destBytes', 'changeBytes', 'vtxoBytes', 'txHex', 'fundingTxHex', 'outputVtxos'];
+ return {
+ v: s.v, mailboxCheckpoint: s.mailboxCheckpoint, nextKeyIndex: s.nextKeyIndex, receiveAckTs: s.receiveAckTs,
+ vtxos: (s.vtxos || []).map((v) => v.state === 'spent'
+ ? { id: v.id, amountSat: v.amountSat, state: 'spent', keyIndex: v.keyIndex, expiryHeight: v.expiryHeight }
+ : v),
+ actions: (s.actions || [])
+ .filter((a) => a.step === 'done' && ['board', 'offboard', 'exit'].includes(a.type))
+ .map((a) => { const c = { ...a }; for (const k of HEAVY) delete c[k]; return c; }),
+ movements: (s.movements || []).slice(-300),
+ gifts: s.gifts,
+ };
+}
+
// Merge two ark states so devices can't clobber each other through the sync
-// snapshot: vtxos union by id with 'spent' winning (a spend on any device
-// sticks), actions/movements/gifts union by id, counters take the max (so two
-// devices never reuse a key index).
+// snapshot: additive union of vtxos (a device keeps its own full copy, gains
+// ones it lacks), actions/movements/gifts union by id, counters take the max
+// (so two devices never reuse a key index).
export function mergeArkStates(a, b) {
if (!a) return b;
if (!b) return a;
@@ -91,7 +114,7 @@ export function arkFeature(ctx) {
mergeAlways: true, // load() is a commutative merge — apply older snapshots too
save: () => {
const s = wallet.loadArkState();
- return s ? { arkState: s } : {};
+ return s ? { arkState: slimArkForSync(s) } : {};
},
load: (d) => {
if (!d.arkState) return;
@@ -182,8 +205,7 @@ export function arkFeature(ctx) {
// an idle device never shared its coins — another device would never see
// them. Publishing on connect guarantees every device's ark balance
// reaches the relay (and gets merged) without needing a transaction first.
- console.log('[arkdbg] connected, vtxos=', (mgr.state && mgr.state.vtxos || []).length, 'net=', getNetwork());
- if (mgr.state && (mgr.state.vtxos || []).length) { try { wallet.saveCache(); console.log('[arkdbg] on-connect saveCache called'); } catch (e) { console.log('[arkdbg] saveCache threw', e.message); } }
+ if (mgr.state && (mgr.state.vtxos || []).length) { try { wallet.saveCache(); } catch {} }
const tick = () => mgr.sync().catch(() => {}).then(() => driveExits(mgr)).catch(() => {});
tick();
// Reconcile once on connect: a vtxo synced in from another device (or
diff --git a/src/features/sync.js b/src/features/sync.js
index 9d361aa..1f3414d 100644
--- a/src/features/sync.js
+++ b/src/features/sync.js
@@ -111,9 +111,8 @@ export function installSyncWallet(wallet) {
// bind the per-device d-tag now: the debounced publish must keep this
// snapshot's network even if the wallet switches networks before it fires
const dtag = deviceDtag(wallet.netName);
- console.log('[syncdbg] cacheSaved scheduling publish', dtag, 'hasArk?', !!snap.arkState, 'sk?', !!wallet.nostr.sk);
clearTimeout(wallet._nostrPubTimer);
- wallet._nostrPubTimer = setTimeout(() => { console.log('[syncdbg] publishing now', dtag); wallet.nostr.publish(snap, dtag); }, 2500);
+ wallet._nostrPubTimer = setTimeout(() => wallet.nostr.publish(snap, dtag), 2500);
});
wallet.registerRealtimeHook({ stop: () => clearTimeout(wallet._nostrPubTimer) });
}
diff --git a/src/nostr.js b/src/nostr.js
index a1d6cc4..e73aa5d 100644
--- a/src/nostr.js
+++ b/src/nostr.js
@@ -163,9 +163,8 @@ export class NostrSync {
try {
const content = nip44.encrypt(JSON.stringify(stateObj), this.ck);
evt = finalizeEvent({ kind: 30078, created_at: Math.floor(Date.now() / 1000), tags: [['d', dtag]], content }, this.sk);
- } catch (e) { console.log('[pubdbg] finalize threw', e.message); return; }
- const res = await Promise.allSettled(pool.publish(this.relays, evt));
- console.log('[pubdbg] publish', dtag, 'bytes', evt.content.length, '->', JSON.stringify(res.map((r) => r.status === 'fulfilled' ? 'ok' : ('rej:' + String(r.reason).slice(0, 80)))));
+ } catch { return; }
+ await Promise.allSettled(pool.publish(this.relays, evt));
}
// Deliver an encrypted DM (a locked gift's claim code) as a NIP-17 gift wrap