1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
// 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');
|