const test = require('node:test'); const assert = require('node:assert'); const L = require('../ladder'); const own = new Set(['nano_own']); const N = '1000000000000000000000000000000'; // 1 XNO in raw const raw = x => (BigInt(Math.round(x * 1000)) * BigInt(N) / 1000n).toString(); const src = (account, receives) => ({ account, amount_raw: receives.reduce((a, r) => a + BigInt(r.amount_raw), 0n).toString(), first_at: receives[0].at, last_at: receives[receives.length - 1].at, receives }); const rcv = (at, x) => ({ at, amount_raw: raw(x), hash: 'H' }); const pay = (ts, x, kind = 'other') => ({ ledger_id: 1, ts, amount_raw: raw(x), kind }); const STAKE = raw(0.16); test('enough moved after the operator paid: at_least_stake and bound both reach the stake', () => { const r = L.amountFunding([src('nano_a', [rcv('2026-09-15T07:25:33.000Z', 0.6), rcv('2026-09-17T04:46:04.000Z', 0.2)])], own, () => [pay('2026-09-09T01:40:01.058Z', 15)], STAKE, []); assert.deepStrictEqual([r.at_least_stake, r.at_least_stake_other, r.bound_at_least_stake], [true, true, true]); assert.strictEqual(r.bound_raw, BigInt(raw(0.8))); assert.strictEqual(r.per_source[0].operator_paid_before_last_transfer_raw, BigInt(raw(15))); assert.strictEqual(r.per_source[0].operator_address, false); assert.strictEqual(r.per_source[0].operator_payments[0].amount_nano, '15'); }); test('sequence passes but the operator paid too little: in order, yet the bound stays below the stake', () => { const r = L.amountFunding([src('nano_a', [rcv('2026-09-15T07:25:33.000Z', 0.6)])], own, () => [pay('2026-09-09T01:40:01.058Z', 0.01)], STAKE, []); assert.strictEqual(r.at_least_stake, true); // the test as proposed: transfers after the first payment >= stake assert.strictEqual(r.bound_raw, BigInt(raw(0.01))); // capped by what the operator actually sent assert.strictEqual(r.bound_at_least_stake, false); }); test('transfers before the operator paid do not count; a dust transfer after it does not reach the stake', () => { const r = L.amountFunding([src('nano_a', [rcv('2026-09-01T00:00:00.000Z', 5), rcv('2026-09-15T00:00:00.000Z', 0.001)])], own, () => [pay('2026-09-10T00:00:00.000Z', 20)], STAKE, []); assert.strictEqual(r.per_source[0].transferred_after_first_operator_payment_raw, BigInt(raw(0.001))); assert.deepStrictEqual([r.at_least_stake, r.bound_at_least_stake], [false, false]); }); test('only a ladder payout before the transfer: counts for any, not for the excluding_ladder_returns twin', () => { const r = L.amountFunding([src('nano_a', [rcv('2026-09-15T00:00:00.000Z', 1)])], own, () => [pay('2026-09-10T00:00:00.000Z', 25, 'ladder_payout')], STAKE, []); assert.deepStrictEqual([r.at_least_stake, r.at_least_stake_other, r.bound_at_least_stake, r.bound_at_least_stake_other], [true, false, true, false]); assert.strictEqual(r.bound_other_raw, 0n); }); test('own address source is labelled operator_address and counts in full; its other figure comes from hop 0, capped', () => { const hop0 = [pay('2026-09-21T12:10:03.012Z', 25.32, 'ladder_payout'), pay('2026-09-21T12:10:05.763Z', 0.32, 'ladder_refund')]; const r = L.amountFunding([src('nano_own', [rcv('2026-09-21T12:12:12.000Z', 0.32), rcv('2026-09-21T12:18:37.000Z', 25.32)])], own, () => [], STAKE, hop0); const p = r.per_source[0]; assert.strictEqual(p.operator_address, true); assert.strictEqual(p.operator_payments, null); assert.strictEqual(p.operator_money_bound_raw, BigInt(raw(25.64))); assert.strictEqual(p.operator_money_bound_other_raw, 0n); assert.deepStrictEqual([r.at_least_stake, r.at_least_stake_other], [true, false]); const o = L.amountFunding([src('nano_own', [rcv('2026-09-21T12:12:12.000Z', 0.5)])], own, () => [], STAKE, [pay('2026-09-20T00:00:00.000Z', 3, 'other')]); assert.strictEqual(o.per_source[0].operator_money_bound_other_raw, BigInt(raw(0.5))); // capped at the source's receives }); test('nulls: unreadable ledger, missing receives, no stake; no sources is false and zero', () => { assert.strictEqual(L.amountFunding([src('nano_a', [rcv('2026-09-15T00:00:00.000Z', 1)])], own, () => null, STAKE, []).at_least_stake, null); assert.strictEqual(L.amountFunding([{ account: 'nano_a', amount_raw: '1', first_at: '2026-09-15T00:00:00.000Z' }], own, () => [pay('2026-09-10T00:00:00.000Z', 1)], STAKE, []).bound_raw, null); assert.strictEqual(L.amountFunding([src('nano_a', [rcv('2026-09-15T00:00:00.000Z', 1)])], own, () => [pay('2026-09-10T00:00:00.000Z', 1)], null, []).at_least_stake, null); assert.strictEqual(L.amountFunding(null, own, () => [], STAKE, []).at_least_stake, null); const z = L.amountFunding([], own, () => [], STAKE, []); assert.deepStrictEqual([z.at_least_stake, z.bound_raw, z.bound_at_least_stake], [false, 0n, false]); }); test('the bound is a minimum of two sums: a big transfer after a small payment is capped, and vice versa', () => { const a = L.amountFunding([src('nano_a', [rcv('2026-09-15T00:00:00.000Z', 100)])], own, () => [pay('2026-09-10T00:00:00.000Z', 2)], STAKE, []); assert.strictEqual(a.bound_raw, BigInt(raw(2))); const b = L.amountFunding([src('nano_a', [rcv('2026-09-15T00:00:00.000Z', 0.05)])], own, () => [pay('2026-09-10T00:00:00.000Z', 100)], STAKE, []); assert.strictEqual(b.bound_raw, BigInt(raw(0.05))); // a payment after the source's last transfer cannot have funded it const c = L.amountFunding([src('nano_a', [rcv('2026-09-15T00:00:00.000Z', 1)])], own, () => [pay('2026-09-10T00:00:00.000Z', 0.1), pay('2026-09-16T00:00:00.000Z', 50)], STAKE, []); assert.strictEqual(c.per_source[0].operator_paid_before_last_transfer_raw, BigInt(raw(0.1))); assert.strictEqual(c.bound_raw, BigInt(raw(0.1))); });