// A renewal started on one device shows on the others as in progress (a phone // knew nothing while the laptop's round ran and the balance sat locked), is // never adopted as an action there, and disappears once it finishes. // Run: bun tools/ark-renewal-sync-test.js import assert from 'node:assert/strict'; import { slimArkForSync, mergeArkStates, remoteRenewalsInFlight } from '../src/features/ark.js'; import { ArkManager } from '../src/ark/manager.js'; const now = Date.now(); const laptop = (step, extra = {}) => ({ serverPubkey: 'srv', vtxos: [], movements: [], actions: [{ id: `refresh-${now - 60_000}`, type: 'refresh', step, inputIds: ['a:0'], outKeyIndex: 9, outAmountSat: 62212, feeSat: 125, unlockHash: 'c0'.repeat(32), fundingTxHex: 'deadbeef', outputVtxos: ['00'], ...extra, }], }); let phone = { serverPubkey: 'srv', vtxos: [], movements: [], actions: [{ id: 'ln-recv-1', type: 'ln-recv', step: 'awaiting' }] }; const snap = slimArkForSync(laptop('submitted')); assert.equal(snap.renewals.length, 1); assert.ok(!('fundingTxHex' in snap.renewals[0]) && !('outputVtxos' in snap.renewals[0]), 'no signing material'); assert.deepEqual(snap.renewals[0].inputIds, ['a:0'], 'names the coins it holds'); phone = mergeArkStates(phone, snap); assert.equal(phone.actions.length, 1, 'never adopted as an action of this device'); assert.equal(remoteRenewalsInFlight(phone).length, 1); assert.equal(remoteRenewalsInFlight(phone)[0].outAmountSat, 62212); assert.ok(!('renewals' in phone), 'the snapshot field is not stored as is'); console.log('✓ The other device sees the renewal in progress, as display only'); phone = mergeArkStates(phone, slimArkForSync(laptop('submitted', { fundingTxid: 'e9'.repeat(32) }))); assert.equal(remoteRenewalsInFlight(phone)[0].fundingTxid, 'e9'.repeat(32)); // an older snapshot (another slot, still at 'created') never moves it back phone = mergeArkStates(phone, slimArkForSync(laptop('created'))); assert.equal(phone.remoteRenewals[0].step, 'submitted'); console.log('✓ It learns the round transaction, and a staler snapshot never moves it back'); phone = mergeArkStates(phone, slimArkForSync(laptop('done', { fundingTxid: 'e9'.repeat(32) }))); assert.equal(remoteRenewalsInFlight(phone).length, 0); console.log('✓ Finished there, it is gone here'); { let p2 = mergeArkStates({ serverPubkey: 'srv', vtxos: [], movements: [], actions: [] }, slimArkForSync(laptop('issued'))); p2 = mergeArkStates(p2, { serverPubkey: 'srv', vtxos: [], actions: [], movements: [{ id: 'm', type: 'refresh', status: 'complete', unlockHash: 'c0'.repeat(32) }] }); assert.equal(remoteRenewalsInFlight(p2).length, 0); console.log('✓ A synced "Renewed" row settles it even before the final telling arrives'); } { const own = laptop('submitted'); const merged = mergeArkStates(own, slimArkForSync(laptop('submitted'))); assert.equal((merged.remoteRenewals || []).length, 0, 'its own renewal never shows twice'); const old = laptop('submitted'); old.actions[0].id = `refresh-${now - 3 * 86_400_000}`; assert.equal(mergeArkStates(phone, slimArkForSync(old)).remoteRenewals.filter((r) => r.id === old.actions[0].id).length, 0); console.log('✓ The device that owns it never sees itself as remote; a renewal begun days ago is dropped'); } { // the phone holds the laptop's renewal coin as spendable in its own records const coin = (id, amountSat) => ({ id, amountSat, state: 'spendable', expiryHeight: 999_999, bytes: 'AA==' }); let st = { serverPubkey: 'srv', vtxos: [coin('a:0', 62337), coin('z:0', 1000)], movements: [], actions: [] }; st = mergeArkStates(st, slimArkForSync(laptop('submitted'))); const mgr = new ArkManager({ storage: { save() {} }, arkUrl: 'http://test.invalid' }); mgr.state = st; mgr._tipH = 1000; assert.deepEqual(mgr.balance(), { spendableSat: 1000, pendingSat: 62337, boardingSat: 0, expiredSat: 0 }); assert.deepEqual(mgr._selectInputs(500).map((v) => v.id), ['z:0']); assert.throws(() => mgr._selectInputs(2000)); await assert.rejects(mgr.refresh(['a:0']), /no spendable/); console.log('✓ Its coins count as pending here and are never offered for a send or a renewal'); st = mergeArkStates(st, slimArkForSync(laptop('failed'))); mgr.state = st; assert.equal(mgr.balance().spendableSat, 63337, 'a renewal that failed there frees them here'); console.log('✓ Failed there, its coins are free again here'); } console.log('\n✅ renewals sync across devices for display');