diff options
| author | Adam Soltys <[email protected]> | 2026-10-30 15:57:46 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-30 15:57:46 -0700 |
| commit | fd60875c2233ad4e367a501825f5e3e37bb2dec6 (patch) | |
| tree | f83532e66b4e1e230709e908a79aa6e046838238 /tools/ark | |
| parent | 5ee7b8ffbb57ca800a145c1ff8ede3aeff5b0dc6 (diff) | |
| download | coinosv3-fd60875c2233ad4e367a501825f5e3e37bb2dec6.tar.gz coinosv3-fd60875c2233ad4e367a501825f5e3e37bb2dec6.zip | |
A wallet that renews its own coins (Settings → Spending: self or exit) pays a Lightning invoice from the coins nearest expiry first, so fewer of them come up for a paid renewal (Stacker News request, 2026-10-29). Lightning sends only: the ASP takes those coins, whereas an Ark-to-Ark send would hand the recipient your short timelocks. The custodial default keeps the smallest coin that covers the payment (expiry costs nothing there), a coin past its timelock still goes last, and a pile of old crumbs that would breach the input cap falls back to largest-first. The closed-app NWC worker follows the mirrored tier the same way
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMan6VLgQdujavxPsfU7A9
Diffstat (limited to 'tools/ark')
| -rw-r--r-- | tools/ark/custody-test.js | 21 |
1 files changed, 21 insertions, 0 deletions
diff --git a/tools/ark/custody-test.js b/tools/ark/custody-test.js index 214ab45..68e7191 100644 --- a/tools/ark/custody-test.js +++ b/tools/ark/custody-test.js @@ -127,4 +127,25 @@ const coin = (id, amountSat, expiryHeight, extra = {}) => ({ id, amountSat, expi console.log('✓ deep coins: no base fee for a refresh of deep coins alone, a capped coin is never spent, the worker orders them flattened'); } +// Lightning sends: a wallet that renews its own coins spends the nearest +// expiry first; the custodial default keeps the smallest coin that covers it +{ + const m = mk(true); + m.info.lnSendFees = { baseFeeSat: 0, minFeeSat: 0, ppmExpiryTable: [] }; + m.state.vtxos = [coin('young', 3000, 2000), coin('old', 9000, 1100), coin('older', 2000, 1050), coin('past', 8000, 900)]; + const ids = (amt) => m._selectLnInputs(m.state.vtxos, 1000, amt, 0).inputs.map((v) => v.id); + assert.deepEqual(ids(2500), ['young'], 'custodial: the smallest coin that covers it'); + assert.deepEqual(ids(12000), ['old', 'young'], 'custodial: largest first'); + m.lnOldestFirst = () => true; + assert.deepEqual(ids(1500), ['older'], 'self: the nearest expiry that covers it'); + assert.deepEqual(ids(2500), ['old'], '…skipping one too small'); + assert.deepEqual(ids(10000), ['older', 'old'], 'self: gathers nearest expiry first'); + assert.deepEqual(ids(13000), ['older', 'old', 'young'], 'coins inside their timelock carry it alone when they can'); + // a pile of old crumbs that would breach the input cap: largest-first instead + m.state.vtxos = [...Array.from({ length: MAX_SEND_INPUTS + 2 }, (_, i) => coin('crumb' + i, 100, 1010)), coin('big', 6000, 3000)]; + assert.deepEqual(ids(6050).slice(0, 1), ['big'], 'too many old crumbs for one payment: largest first'); + assert.equal(ids(6050).length, 2); + console.log('✓ Lightning sends: nearest expiry first when renewing your own coins, smallest-that-covers otherwise'); +} + console.log('\n✅ custody tiers behave'); |
