aboutsummaryrefslogtreecommitdiffstats
path: root/tools/ark/refresh-reprice-test.js
blob: 2936036fb7ac0e8036a51ebfc533b9380ed0351f (plain) (blame)
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
// A renewal priced from a fee schedule the server has since raised (an app
// left open across the change) is re-priced and resubmitted, not refused on
// every sync forever; one the new fee can't cover gives its coins back.
// Run: bun tools/ark/refresh-reprice-test.js
import assert from 'node:assert/strict';
import { mock } from 'bun:test';

const refresh = await import('../../src/ark/refresh.js');
const proto = await import('../../src/ark/proto.js');
const send = await import('../../src/ark/send.js');
mock.module('../../src/ark/send.js', () => ({ ...send, registerVtxoTransactions: async () => {} }));
let fees = { baseFeeSat: 300, ppmExpiryTable: [{ thresholdBlocks: 0, ppm: 0 }] };
mock.module('../../src/ark/refresh.js', () => ({
  ...refresh,
  submitRoundParticipation: async () => { throw new Error('bad user input: insufficient refresh fee: expected at least 300 sat, got 0 sat'); },
}));
mock.module('../../src/ark/proto.js', () => ({
  ...proto,
  getArkInfo: async () => ({ refreshFees: fees }),
}));
const { ArkManager } = await import('../../src/ark/manager.js');
const mgr = new ArkManager({ storage: { save() {} }, arkUrl: 'http://test.invalid' });
Object.defineProperty(mgr, 'chain', { value: { tipHeight: async () => 1000 } });
mgr._refreshOutputs = () => [];
mgr._key = () => ({});
mgr._mailboxKey = () => ({ pubkey: new Uint8Array(33) });
mgr._decoded = () => ({});
mgr._keyForVtxo = () => ({});
mgr._tipH = 1000;

{
  mgr.info = { refreshFees: { baseFeeSat: 0, ppmExpiryTable: [{ thresholdBlocks: 0, ppm: 0 }] } };
  const action = { id: 'refresh-1', type: 'refresh', step: 'created', inputIds: ['a:0'], outKeyIndex: 1, outAmountSat: 5000, feeSat: 0 };
  mgr.state = { nextKeyIndex: 2, movements: [], actions: [action], vtxos: [{ id: 'a:0', amountSat: 5000, expiryHeight: 9000, state: 'pending', bytes: 'AA==' }] };
  await mgr._driveRefresh(action);
  assert.equal(action.step, 'created', 'kept for the next sync to resubmit');
  assert.equal(action.feeSat, 300);
  assert.equal(action.outAmountSat, 4700);
  assert.equal(mgr.info.refreshFees.baseFeeSat, 300, 'the new schedule is now the wallet’s');
  console.log('✓ A renewal priced before a fee rise is re-priced at the new fee');
  // the same refusal again, nothing left to change: give up rather than loop
  await mgr._driveRefresh(action);
  assert.equal(action.step, 'failed');
  assert.equal(mgr.state.vtxos[0].state, 'spendable');
  console.log('✓ Refused again at the new price, it stops and frees the coins');
}

{
  mgr.info = { refreshFees: { baseFeeSat: 0, ppmExpiryTable: [{ thresholdBlocks: 0, ppm: 0 }] } };
  const action = { id: 'refresh-2', type: 'refresh', step: 'created', inputIds: ['b:0'], outKeyIndex: 1, outAmountSat: 500, feeSat: 0 };
  mgr.state = { nextKeyIndex: 2, movements: [], actions: [action], vtxos: [{ id: 'b:0', amountSat: 500, expiryHeight: 9000, state: 'pending', bytes: 'AA==' }] };
  await mgr._driveRefresh(action);
  assert.equal(action.step, 'failed');
  assert.equal(action.superseded, true, 'no history row for a renewal that never began');
  assert.equal(mgr.state.vtxos[0].state, 'spendable');
  console.log('✓ A balance the new fee leaves under 330 sats gets its coins back');
}
console.log('\n✅ refresh re-pricing passes');