import { describe, expect, it } from 'vitest'; import { adjustRuneAmountsToAvoidFailure, subtractBytesFromAmount, clampAmountsToFairness, adjustRuneAmountsToAvoidRunestoneLimit, buildRunestoneFromAmounts, } from '../runestone'; describe('subtractBytesFromAmount', () => { it('Returns newAmount 0 when desired encoded length is less than or equal to 0', () => { const amount = 100n; // 100 in binary is "1100100" (7 bits, encoded length = 1). const bytesToRemove = 2; // desiredEncodedLength = 1 - 2 = -1. const result = subtractBytesFromAmount(amount, bytesToRemove); expect(result.newAmount).toBe(0n); expect(result.amountRemoved).toBe(amount); expect(result.bytesRemoved).toBe(1); }); it('Returns the same amount if no subtraction is needed', () => { const amount = 5000n; // approximate bitLength 13, ceil(13/7)=2, desired = 2-1=1, maxAmountForDesiredLength=127, 5000 > 127 => subtraction will occur. // We need a case where subtraction does not occur. For that, choose bytesToRemove=0. const result = subtractBytesFromAmount(amount, 0); expect(result.newAmount).toBe(amount); expect(result.amountRemoved).toBe(0n); expect(result.bytesRemoved).toBe(0); }); it('Returns adjusted amount when subtraction is needed', () => { // 1000 in binary is about 10 bits, so encoded length = ceil(10/7)=2. // For bytesToRemove = 1, desiredEncodedLength = 2-1 = 1. // maxAmountForDesiredLength = (1 << 7) - 1 = 127n. // Since 1000n > 127n, subtraction occurs. const amount = 1000n; const bytesToRemove = 1; const result = subtractBytesFromAmount(amount, bytesToRemove); // Expect newAmount = 127n, amountRemoved = 1000n - 127n. expect(result.newAmount).toBe(127n); expect(result.amountRemoved).toBe(873n); expect(result.bytesRemoved).toBe(bytesToRemove); }); }); describe('adjustRuneAmountsToAvoidFailure', () => { it('Returns the original array if only one amount is provided', () => { const amounts = [1000n]; const threshold = 10n; const result = adjustRuneAmountsToAvoidFailure(amounts, threshold); expect(result).toEqual(amounts); }); it('Returns unchanged when denominator is zero (all mass in one bucket)', () => { const amounts = [0n, 1000n, 0n]; const threshold = 10n; const result = adjustRuneAmountsToAvoidFailure(amounts, threshold); expect(result).toEqual([0n, 1000n, 0n]); }); it('Proportionally adjusts amounts for UpdateLiquidityBy (normal case)', () => { // Two elements: 500n and 1500n, total = 2000n. // Largest is 1500n (index 1); adjustmentAmount = (2000*10)/100 = 200n. // Denom = 2000 - 1500 = 500n. // For largest: new = 1500 + 200 = 1700n. // For the other: proportion = (500 * 200) / 500 = 200, so new = 500 - 200 = 300n. const amounts = [500n, 1500n]; const threshold = 10n; const result = adjustRuneAmountsToAvoidFailure(amounts, threshold); expect(result).toEqual([300n, 1700n]); expect(result[0] + result[1]).toBe(2000n); }); it('Preserves the total sum despite rounding errors', () => { // Use values that may lead to rounding adjustments. // For example: amounts = [400n, 600n, 800n] => total = 1800n. // Threshold 7%: adjustmentAmount = (1800 * 7)/100 = 126n. // Largest is 800n (index 2), denominator = 1800 - 800 = 1000n. // For index2: new = 800 + 126 = 926n. // For index0: proportion = (400 * 126)/1000 = 50.4 -> 50n, new = 400 - 50 = 350n. // For index1: proportion = (600 * 126)/1000 = 75.6 -> 75n, new = 600 - 75 = 525n. // Sum = 350 + 525 + 926 = 1801n, difference = 1800 - 1801 = -1n. // Difference added to largest: 926 - 1 = 925n. // Final result: [350n, 525n, 925n], total = 350+525+925 = 1800n. const amounts = [400n, 600n, 800n]; const threshold = 7n; const result = adjustRuneAmountsToAvoidFailure(amounts, threshold); expect(result.reduce((sum, a) => sum + a, 0n)).toBe(1800n); expect(result[2]).toBe(925n); }); it('Returns unchanged amounts when threshold is 0', () => { const amounts = [300n, 700n]; const threshold = 0n; const result = adjustRuneAmountsToAvoidFailure(amounts, threshold); expect(result).toEqual(amounts); }); it('Handles proportional distribution when remainingAmount is insufficient', () => { const amounts = [1000n, 2000n]; const threshold = 10n; const result = adjustRuneAmountsToAvoidFailure(amounts, threshold); expect(result).toEqual([700n, 2300n]); expect(result.reduce((sum, a) => sum + a, 0n)).toBe(3000n); }); }); describe('clampAmountsToFairness', () => { it('Keeps amounts within ±50% of ideal, preserving total (simple case)', () => { const amounts = [41n, 41n, 41n, 42n, 144n, 41n]; // total = 350, n=6, ideal=58, acceptable=29 => range [29,87] const clamped = clampAmountsToFairness(amounts, 50n); const total = clamped.reduce((s, a) => s + a, 0n); expect(total).toBe(350n); for (const a of clamped) { expect(a >= 29n && a <= 87n).toBe(true); } }); it('No-op when already within bounds', () => { const amounts = [58n, 58n, 58n, 58n]; const clamped = clampAmountsToFairness(amounts, 50n); expect(clamped).toEqual(amounts); }); it('Reduces oversized and raises undersized while preserving total (skewed case)', () => { const amounts = [0n, 0n, 0n, 350n]; const clamped = clampAmountsToFairness(amounts, 50n); const total = clamped.reduce((s, a) => s + a, 0n); expect(total).toBe(350n); const n = BigInt(clamped.length); const ideal = 350n / n; // 87 const acceptable = (50n * ideal + 99n) / 100n; // 44 const minVal = ideal - acceptable; // >= 43 const maxVal = ideal + acceptable; // <= 131 for (const a of clamped) { expect(a >= minVal && a <= maxVal).toBe(true); } }); it('Handles extreme all-in-one skew, keeping values within bounds and total', () => { const total = 1000n; const amounts = [0n, 0n, 0n, total]; const clamped = clampAmountsToFairness(amounts, 50n); expect(clamped.reduce((s, a) => s + a, 0n)).toBe(total); const ideal = total / 4n; const acceptable = (50n * ideal + 99n) / 100n; const minVal = ideal - acceptable; const maxVal = ideal + acceptable; for (const a of clamped) { expect(a >= minVal && a <= maxVal).toBe(true); } }); }); describe('adjustRuneAmountsToAvoidRunestoneLimit', () => { const token: any = { block: 55049n, tx: 12 }; it('No change when the runestone fits under the limit', () => { const amounts = [100n, 80n, 60n]; const pointer = 0; // Build once to get current size and use that as generous limit const size = buildRunestoneFromAmounts(token, amounts.slice(), pointer) .encipher().length; const result = adjustRuneAmountsToAvoidRunestoneLimit( token, amounts.slice(), pointer, size, ); expect(result).toEqual(amounts); }); it('Shrinks edict amounts so that runestone size fits under a tight limit', () => { const amounts = [2000n, 2000n, 2000n]; const pointer = 0; const base = buildRunestoneFromAmounts(token, amounts.slice(), pointer) .encipher(); // Force a smaller limit than current to trigger shrinking const limit = Math.max(1, base.length - 2); const result = adjustRuneAmountsToAvoidRunestoneLimit( token, amounts.slice(), pointer, limit, ); const after = buildRunestoneFromAmounts(token, result.slice(), pointer) .encipher(); // New size must be <= limit expect(after.length).toBeLessThanOrEqual(limit); // Total amount preserved expect(result.reduce((s, a) => s + a, 0n)).toBe( amounts.reduce((s, a) => s + a, 0n), ); // At least one non-pointer edict should reduce (since tight limit) expect(result.some((a, i) => i > 0 && a < amounts[i])).toBe(true); }); });