aboutsummaryrefslogtreecommitdiffstats
path: root/tools/ark-reconcile-race.test.js
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-04 08:58:34 -0700
committerAdam Soltys <[email protected]>2026-10-04 08:58:34 -0700
commit7421218e987b0053f1ddbefaddc2c9ecfd664954 (patch)
tree4d04e81afa32c230e7f2d8beb1f723662a4747c2 /tools/ark-reconcile-race.test.js
parent7c6ca4d70b91dc2d050cb18e7978b65bc1db2d59 (diff)
downloadcoinosv3-7421218e987b0053f1ddbefaddc2c9ecfd664954.tar.gz
coinosv3-7421218e987b0053f1ddbefaddc2c9ecfd664954.zip
Ark: a double-tapped zap no longer spends one coin twice
A reconcile pass that began before the first zap took its coin handed it back as spendable (the server still said so) while the zap was in flight; the second zap picked it and the server refused: 'vtxo … already spent'. Coins whose state changed during the pass are left alone. And a payment refused for an already-spent coin now reconciles and pays once more instead of failing with the raw error. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_011bvzKaZosXGHkigcGcSzAR
Diffstat (limited to 'tools/ark-reconcile-race.test.js')
-rw-r--r--tools/ark-reconcile-race.test.js22
1 files changed, 22 insertions, 0 deletions
diff --git a/tools/ark-reconcile-race.test.js b/tools/ark-reconcile-race.test.js
new file mode 100644
index 0000000..165eb58
--- /dev/null
+++ b/tools/ark-reconcile-race.test.js
@@ -0,0 +1,22 @@
+// bun test tools/ark-reconcile-race.test.js — a reconcile pass must not hand a
+// coin back to spendable after a payment took it mid-pass (double-tapped zap).
+import { test, expect, mock } from 'bun:test';
+import * as proto from '../src/ark/proto.js';
+let release;
+const gate = new Promise((r) => { release = r; });
+mock.module('../src/ark/proto.js', () => ({ ...proto, getVtxoStatus: async () => { await gate; return proto.VTXO_STATE_SPENDABLE; } }));
+const { ArkManager } = await import('../src/ark/manager.js');
+test('a coin taken mid-reconcile stays taken', async () => {
+ const m = Object.create(ArkManager.prototype);
+ m.state = { vtxos: [{ id: 'a', state: 'spendable', amountSat: 85, bytes: 'x' }, { id: 'b', state: 'pending', amountSat: 5, bytes: 'x' }], actions: [], scheduled: [] };
+ m.arkUrl = 'x'; m._decoded = () => ({ point: { raw: new Uint8Array(36) } }); m._keyForVtxo = () => ({ privkey: new Uint8Array(32) });
+ m._save = () => {}; m._movement = () => {};
+ const p = m.reconcile();
+ await new Promise((r) => setTimeout(r, 10));
+ m.state.vtxos[0].state = 'pending'; // the zap takes coin a
+ m.state.actions.push({ type: 'ln-pay', step: 'created', parts: [{ inputId: 'a' }] });
+ release();
+ await p;
+ expect(m.state.vtxos[0].state).toBe('pending');
+ expect(m.state.vtxos[1].state).toBe('spendable'); // a real orphan is still freed
+});