import { Token } from '@saturnbtcio/pool-serde-sdk'; import { PoolErrorException, PoolErrorType } from '../error/pool.error'; import { Edict, RuneId, Runestone } from '@saturnbtcio/ordinals-lib'; export const adjustRuneAmountsToAvoidFailure = ( amounts: bigint[], threshold: bigint, ) => { if (amounts.length <= 1) return amounts; // Find the total amount and the index of the largest amount const totalAmount = amounts.reduce((sum, amount) => sum + amount, 0n); const largestIndex = amounts.reduce( (maxIndex, current, index, arr) => current > arr[maxIndex] ? index : maxIndex, 0, ); const denominator = totalAmount - amounts[largestIndex]; if (denominator === 0n) { // All the amount is concentrated in the largest amount return amounts; } // Calculate the adjustment amount (threshold % of total) const adjustmentAmount = (totalAmount * threshold) / 100n; // Calculate initial adjustments const adjustedAmounts = amounts.map((amount, index) => { if (index === largestIndex) { return amount + adjustmentAmount; } else { const proportion = (amount * adjustmentAmount) / denominator; return amount - proportion; } }); // Calculate the difference from rounding errors const newTotal = adjustedAmounts.reduce((sum, amount) => sum + amount, 0n); const difference = totalAmount - newTotal; // Add any rounding difference to the largest amount to maintain the exact total if (difference !== 0n) { adjustedAmounts[largestIndex] += difference; } console.log('adjustedAmounts', adjustedAmounts); return adjustedAmounts; }; export const buildRunestoneFromAmounts = ( token: Token, amounts: bigint[], pointer: number | undefined, ) => { // Sort the amounts from biggest to smallest. amounts.sort((a, b) => Number(b - a)); const runestone = Runestone.default(); runestone.pointer = pointer; // Skipping the first one because it goes to the pointer. const edicts: Edict[] = []; for (let i = 1; i < amounts.length; i++) { // We don't care about real output because we're just // measuring the byte length. const runeId = new RuneId(token.block, token.tx); const edict = new Edict(runeId, amounts[i], i); edicts.push(edict); } runestone.edicts = edicts; return runestone; }; export const adjustRuneAmountsToAvoidRunestoneLimit = ( token: Token, amounts: bigint[], pointer: number | undefined, limit: number, ) => { const runestone = buildRunestoneFromAmounts(token, amounts, pointer); const enciphered = runestone.encipher(); const size = enciphered.length; if (size > limit) { if (runestone.edicts.length <= 1) { throw new PoolErrorException({ type: PoolErrorType.InvalidRunestone, message: 'Built a runestone with an invalid size', }); } // We need to make the other amounts smaller. const offset = size - limit; const bytePerEdict = Math.ceil(offset / (amounts.length - 1)); let totalBytesRemoved = 0; for (let i = 1; i < amounts.length; i++) { const { newAmount, amountRemoved, bytesRemoved } = subtractBytesFromAmount(amounts[i], bytePerEdict); amounts[i] = newAmount; amounts[0] = amounts[0] + amountRemoved; totalBytesRemoved += bytesRemoved; } // There has been a few that we couldn't remove. if (offset > totalBytesRemoved) { let bytesRemaining = totalBytesRemoved; // Loop again but this time removing all we can. for (let i = 1; i < amounts.length; i++) { const { newAmount, amountRemoved, bytesRemoved } = subtractBytesFromAmount(amounts[i], bytePerEdict); amounts[i] = newAmount; amounts[0] = amounts[0] + amountRemoved; bytesRemaining -= bytesRemoved; if (bytesRemaining === 0) { break; } } } } console.log('amounts', amounts); return amounts; }; export const clampAmountsToFairness = ( amounts: bigint[], thresholdPercent: bigint, // e.g. 50n → 50% ): bigint[] => { if (amounts.length === 0) return []; const total = amounts.reduce((s, a) => s + a, 0n); const n = BigInt(amounts.length); const ideal = total / n; const acceptableDeviation = (thresholdPercent * ideal + 99n) / 100n; const minVal = ideal - acceptableDeviation; const maxVal = ideal + acceptableDeviation; const clamped = amounts.map((a) => (a < minVal ? minVal : a > maxVal ? maxVal : a)); let sum = clamped.reduce((s, a) => s + a, 0n); if (sum === total) return clamped; if (sum < total) { // Need to add (total - sum) by pushing up towards max let add = total - sum; for (let i = 0; i < clamped.length && add > 0n; i++) { const room = maxVal - clamped[i]; if (room <= 0n) continue; const step = room < add ? room : add; clamped[i] += step; add -= step; } sum = clamped.reduce((s, a) => s + a, 0n); return clamped; } else { // Need to remove (sum - total) by pulling down towards min let remove = sum - total; for (let i = 0; i < clamped.length && remove > 0n; i++) { const room = clamped[i] - minVal; if (room <= 0n) continue; const step = room < remove ? room : remove; clamped[i] -= step; remove -= step; } return clamped; } }; export const subtractBytesFromAmount = ( amount: bigint, bytesToRemove: number, ) => { // Helper function to compute the bit length of a bigint const bitLength = (n: bigint): number => { if (n === 0n) return 0; return n.toString(2).length; }; const originalBitLength = bitLength(amount); const originalEncodedLength = Math.ceil(originalBitLength / 7); const desiredEncodedLength = originalEncodedLength - bytesToRemove; // If desired encoded length is less than or equal to zero, set newAmount to zero if (desiredEncodedLength <= 0) { return { newAmount: 0n, amountRemoved: amount, bytesRemoved: originalEncodedLength, }; } // Maximum value that fits in the desired number of bytes const maxBitLength = desiredEncodedLength * 7; const maxAmountForDesiredLength = (1n << BigInt(maxBitLength)) - 1n; // If the amount is already less than or equal to the maximum, no subtraction needed if (amount <= maxAmountForDesiredLength) { return { newAmount: amount, amountRemoved: 0n, bytesRemoved: 0, }; } const newAmount = maxAmountForDesiredLength; const amountRemoved = amount - newAmount; return { newAmount, amountRemoved, bytesRemoved: bytesToRemove, }; };