post-app-website-new/lib/tbff/algorithms.ts

237 lines
6.6 KiB
TypeScript

/**
* Flow Funding algorithms
* Implements the mathematical model from threshold-based-flow-funding.md
*/
import type { FlowFundingNetwork, FlowFundingAccount } from './types'
import { updateAccountComputedProperties, calculateNetworkTotals } from './utils'
/**
* Initial distribution of external funding to accounts
*
* Algorithm:
* 1. Calculate total shortfall (funds needed to reach minimums)
* 2. If funding < shortfall: distribute proportionally to shortfalls
* 3. If funding >= shortfall: fill all minimums first, then distribute remaining by capacity
*
* @param network - Current network state
* @param externalFunding - Amount of new funding to distribute
* @returns Updated network with new balances
*/
export function initialDistribution(
network: FlowFundingNetwork,
externalFunding: number
): FlowFundingNetwork {
if (externalFunding <= 0) {
console.warn('⚠️ No funding to distribute')
return network
}
console.log(`\n💰 Initial Distribution: ${externalFunding} funding`)
console.log('━'.repeat(50))
// Calculate total shortfall (funds needed to reach minimums)
const totalShortfall = network.accounts.reduce(
(sum, acc) => sum + Math.max(0, acc.minThreshold - acc.balance),
0
)
console.log(`Total shortfall: ${totalShortfall.toFixed(2)}`)
if (externalFunding < totalShortfall) {
// Not enough to cover all minimums - distribute proportionally
console.log('⚠️ Insufficient funding to cover all minimums')
console.log('Distributing proportionally by shortfall...\n')
return distributeProportionallyByShortfall(network, externalFunding, totalShortfall)
} else {
// Enough funding - fill minimums first, then distribute by capacity
console.log('✓ Sufficient funding to cover all minimums')
console.log('Step 1: Filling all minimums...')
const afterMinimums = fillAllMinimums(network)
const remainingFunds = externalFunding - totalShortfall
console.log(`Remaining funds: ${remainingFunds.toFixed(2)}`)
console.log('Step 2: Distributing by capacity...\n')
return distributeByCapacity(afterMinimums, remainingFunds)
}
}
/**
* Distribute funding proportionally to shortfalls
* Used when funding is insufficient to cover all minimums
*/
function distributeProportionallyByShortfall(
network: FlowFundingNetwork,
funding: number,
totalShortfall: number
): FlowFundingNetwork {
const updatedAccounts = network.accounts.map((acc) => {
const shortfall = Math.max(0, acc.minThreshold - acc.balance)
if (shortfall === 0) return acc
const share = (shortfall / totalShortfall) * funding
const newBalance = acc.balance + share
console.log(
` ${acc.name.padEnd(15)} ${acc.balance.toFixed(0)}${newBalance.toFixed(0)} (+${share.toFixed(0)})`
)
return updateAccountComputedProperties({
...acc,
balance: newBalance,
})
})
return calculateNetworkTotals({
...network,
accounts: updatedAccounts,
})
}
/**
* Fill all accounts to their minimum thresholds
*/
function fillAllMinimums(network: FlowFundingNetwork): FlowFundingNetwork {
const updatedAccounts = network.accounts.map((acc) => {
const shortfall = Math.max(0, acc.minThreshold - acc.balance)
if (shortfall === 0) {
console.log(` ${acc.name.padEnd(15)} ${acc.balance.toFixed(0)} (already at minimum)`)
return acc
}
const newBalance = acc.minThreshold
console.log(
` ${acc.name.padEnd(15)} ${acc.balance.toFixed(0)}${newBalance.toFixed(0)} (+${shortfall.toFixed(0)})`
)
return updateAccountComputedProperties({
...acc,
balance: newBalance,
})
})
return calculateNetworkTotals({
...network,
accounts: updatedAccounts,
})
}
/**
* Distribute funding proportionally to account capacities
* Capacity = max(0, maxThreshold - balance)
*/
function distributeByCapacity(
network: FlowFundingNetwork,
funding: number
): FlowFundingNetwork {
if (funding <= 0) {
console.log(' No remaining funds to distribute')
return network
}
// Calculate total capacity
const totalCapacity = network.accounts.reduce(
(sum, acc) => sum + Math.max(0, acc.maxThreshold - acc.balance),
0
)
if (totalCapacity === 0) {
// All accounts at max - distribute evenly (will create overflow)
console.log(' All accounts at max capacity - distributing evenly (will overflow)')
return distributeEvenly(network, funding)
}
// Distribute proportionally to capacity
const updatedAccounts = network.accounts.map((acc) => {
const capacity = Math.max(0, acc.maxThreshold - acc.balance)
if (capacity === 0) {
console.log(` ${acc.name.padEnd(15)} ${acc.balance.toFixed(0)} (at max capacity)`)
return acc
}
const share = (capacity / totalCapacity) * funding
const newBalance = acc.balance + share
console.log(
` ${acc.name.padEnd(15)} ${acc.balance.toFixed(0)}${newBalance.toFixed(0)} (+${share.toFixed(0)})`
)
return updateAccountComputedProperties({
...acc,
balance: newBalance,
})
})
return calculateNetworkTotals({
...network,
accounts: updatedAccounts,
})
}
/**
* Distribute funding evenly across all accounts
* Used when all accounts are at max capacity
*/
function distributeEvenly(
network: FlowFundingNetwork,
funding: number
): FlowFundingNetwork {
const perAccount = funding / network.accounts.length
const updatedAccounts = network.accounts.map((acc) => {
const newBalance = acc.balance + perAccount
console.log(
` ${acc.name.padEnd(15)} ${acc.balance.toFixed(0)}${newBalance.toFixed(0)} (+${perAccount.toFixed(0)})`
)
return updateAccountComputedProperties({
...acc,
balance: newBalance,
})
})
return calculateNetworkTotals({
...network,
accounts: updatedAccounts,
})
}
/**
* Calculate distribution summary (for UI display)
*/
export function getDistributionSummary(
beforeNetwork: FlowFundingNetwork,
afterNetwork: FlowFundingNetwork
): {
totalDistributed: number
accountsChanged: number
changes: Array<{ accountId: string; name: string; before: number; after: number; delta: number }>
} {
const changes = afterNetwork.accounts.map((after) => {
const before = beforeNetwork.accounts.find((a) => a.id === after.id)!
const delta = after.balance - before.balance
return {
accountId: after.id,
name: after.name,
before: before.balance,
after: after.balance,
delta,
}
}).filter(c => c.delta !== 0)
const totalDistributed = changes.reduce((sum, c) => sum + c.delta, 0)
const accountsChanged = changes.length
return {
totalDistributed,
accountsChanged,
changes,
}
}