aboutsummaryrefslogtreecommitdiffstats
path: root/tools/ark-store-test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-10 07:18:09 -0700
committerAdam Soltys <[email protected]>2026-09-10 07:18:09 -0700
commitaf4810e2ec8e454e4bb616c685e907eaee04c874 (patch)
treeb21bf495f74caa04c11cff951767719e3229de2f /tools/ark-store-test.js
parent6113cf567fa0830233d5c3d524361f24a3f78caa (diff)
downloadcoinosv3-af4810e2ec8e454e4bb616c685e907eaee04c874.tar.gz
coinosv3-af4810e2ec8e454e4bb616c685e907eaee04c874.zip
Ark state moves to IndexedDB: raw bytes on disk, memory-fronted, write-behind.
localStorage held a deep wallet's ark state as ~700KB of JSON (UTF-16 on disk), parsed on every boot and stringified on every save, and the wallet cache blob carried a second ~600KB copy. src/ark/store.js keeps the state in IndexedDB (db coinos-ark) with each coin's signed bytes as a Uint8Array, fronted by an in-memory map so loadArkState/saveArkState stay synchronous: reads return clones, writes coalesce (200ms) and flush immediately when the page hides. Existing localStorage copies migrate on first open (newest _rev wins) and are removed only after the IndexedDB write completed; no IndexedDB means localStorage exactly as before. Records keep their btc-wallet-cache:<id>:ark… keys, and a wallet wipe clears both stores via the new per-feature wipeCache hook. The ark cache extension is now syncOnly: it still rides the sync snapshot (and the Blossom blob) but the local wallet cache no longer embeds it — measured on a live wallet the cache blob went from 655KB to 10KB. Verified in the live tab: all wallets migrated, bytes raw on disk, balance and coin set unchanged, localStorage ark keys gone. tools/ark-store-test.js covers migration by _rev, clone-on-read, write-behind, wipe and fallback. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01Nw1Dijik1tLVKRM1WkU7xE
Diffstat (limited to 'tools/ark-store-test.js')
-rw-r--r--tools/ark-store-test.js90
1 files changed, 90 insertions, 0 deletions
diff --git a/tools/ark-store-test.js b/tools/ark-store-test.js
new file mode 100644
index 0000000..4e02fa0
--- /dev/null
+++ b/tools/ark-store-test.js
@@ -0,0 +1,90 @@
+// Ark state store: memory-fronted IndexedDB (a memory backend here) with
+// localStorage migration by _rev, raw bytes on disk, clone-on-read,
+// write-behind flush, wipe by prefix, and the localStorage fallback.
+// Run: bun tools/ark-store-test.js
+import assert from 'node:assert/strict';
+import { ArkStore, memoryBackend } from '../src/ark/store.js';
+import { vtxoBytesToStr } from '../src/ark/proto.js';
+
+class Storage {
+ data = new Map();
+ get length() { return this.data.size; }
+ key(i) { return [...this.data.keys()][i] ?? null; }
+ getItem(k) { return this.data.get(k) ?? null; }
+ setItem(k, v) { this.data.set(k, String(v)); }
+ removeItem(k) { this.data.delete(k); }
+}
+const raw = new Uint8Array(64).map((_, i) => i * 3);
+const b64 = vtxoBytesToStr(raw);
+const K = 'btc-wallet-cache:abc123:ark:fsobdp';
+const HOLD = 'btc-wallet-cache:abc123:ark:pk:03ff959f88f41d5f';
+const OTHER = 'btc-wallet-cache:zzz999:ark:fsobdp';
+
+// ---- migration: newest _rev wins, localStorage retired after the write ----
+const ls = new Storage();
+ls.setItem(K, JSON.stringify({ _rev: 7, vtxos: [{ id: 'a', state: 'spendable', bytes: b64 }], actions: [], movements: [] }));
+ls.setItem(HOLD, JSON.stringify({ vtxos: [{ id: 'h', state: 'spendable', bytes: b64 }] }));
+ls.setItem('btc-wallet-cache:abc123', '{"txs":[]}'); // the wallet cache is not ours
+const backend = memoryBackend([[K, { _rev: 5, vtxos: [{ id: 'a', state: 'spendable', bytes: raw }], actions: [], movements: [] }],
+ [OTHER, { _rev: 1, vtxos: [], actions: [], movements: [] }]]);
+const store = new ArkStore({ backend, ls: () => ls });
+assert.equal(store.ready, false);
+await store.open();
+assert.equal(store.get(K)._rev, 7, 'the newer localStorage copy wins');
+assert.equal(store.get(HOLD).vtxos[0].bytes, b64, 'hold state migrates too');
+await store.flush();
+await new Promise((r) => setTimeout(r, 0));
+assert.equal(ls.getItem(K), null, 'localStorage copy retired after the write');
+assert.equal(ls.getItem(HOLD), null);
+assert.equal(ls.getItem('btc-wallet-cache:abc123'), '{"txs":[]}', 'unrelated keys untouched');
+assert.ok(backend.dump().get(K).vtxos[0].bytes instanceof Uint8Array, 'bytes are raw on disk');
+assert.equal(store.get(K).vtxos[0].bytes, b64, 'and base64 strings in memory');
+console.log('✓ localStorage state migrates by _rev, bytes land raw, old copies retired');
+
+// ---- reads are clones; writes are write-behind ----
+const got = store.get(K); got.vtxos.push({ id: 'evil' });
+assert.equal(store.get(K).vtxos.length, 1, 'a caller cannot mutate the store through a read');
+const writes = backend.writes;
+store.set(K, { _rev: 8, vtxos: [{ id: 'a', state: 'spent', bytes: b64 }, { id: 'b', state: 'spendable', bytes: b64 }], actions: [], movements: [] });
+assert.equal(store.get(K)._rev, 8, 'memory is current immediately');
+assert.equal(backend.writes, writes, 'disk write is deferred');
+await store.flush();
+assert.equal(backend.dump().get(K)._rev, 8);
+assert.equal(backend.dump().get(K).vtxos.length, 2);
+store.remove(OTHER);
+await store.flush();
+assert.equal(backend.dump().has(OTHER), false);
+console.log('✓ clone-on-read, coalesced write-behind, delete');
+
+// ---- a fresh open reads back what was written ----
+const again = new ArkStore({ backend, ls: () => new Storage() });
+await again.open();
+assert.equal(again.get(K).vtxos[1].bytes, b64, 'round trip through raw bytes');
+assert.deepEqual(again.keys('btc-wallet-cache:abc123').sort(), [K, HOLD].sort(), 'keys by prefix');
+console.log('✓ reopen restores state');
+
+// ---- wipe by wallet prefix (both stores) ----
+const ls2 = new Storage(); ls2.setItem('btc-wallet-cache:abc123:ark', '{"vtxos":[]}');
+const wipe = new ArkStore({ backend, ls: () => ls2 });
+await wipe.open(); await wipe.flush();
+wipe.removePrefix('btc-wallet-cache:abc123');
+await wipe.flush();
+assert.equal(wipe.keys('btc-wallet-cache:abc123').length, 0);
+assert.equal(backend.dump().has(K), false);
+assert.equal(ls2.getItem('btc-wallet-cache:abc123:ark'), null);
+console.log('✓ wallet wipe clears IndexedDB and localStorage copies');
+
+// ---- no IndexedDB: localStorage exactly as before ----
+const ls3 = new Storage();
+const fallback = new ArkStore({ backend: null, ls: () => ls3 });
+assert.equal(fallback.ready, true);
+fallback.set(K, { _rev: 1, vtxos: [] });
+assert.equal(JSON.parse(ls3.getItem(K))._rev, 1);
+assert.equal(fallback.get(K)._rev, 1);
+const broken = new ArkStore({ backend: { list: async () => { throw new Error('nope'); }, write: async () => {} }, ls: () => ls3 });
+await broken.open();
+assert.equal(broken.lsMode, true, 'a refusing IndexedDB drops to localStorage');
+assert.equal(broken.get(K)._rev, 1);
+console.log('✓ localStorage fallback');
+
+console.log('\n✅ ark store behaves');