import { coalitionGroups, type CoalitionSeat, type CoalitionState } from './coalition-alliance'; /** Already-escrowed spice, explicitly pledged by one donor to one ally. * Wallet debits, pledge authorization, commitments and refunds remain the * engine's responsibility; this ledger does not create or hold spice itself. */ export type CoalitionPledge = Readonly<{ donor: string; recipient: string; amount: number; /** The particular alliance edge under which this escrow was authorized. */ epoch: string; }>; export type CoalitionFundingAuthorization = Readonly<{ /** Sorted pair of currently allied seats. */ members: readonly [string, string]; epoch: string; }>; export type CoalitionContribution = Readonly<{ donor: string; amount: number }>; export type CoalitionFundingInput = Readonly<{ seats: readonly CoalitionSeat[]; coalitionState?: CoalitionState; purpose: 'bid' | 'shipment'; payer: string; cost: number; /** Spendable payer spice after other obligations have been reserved. */ ownAvailable: number; /** Authoritative current edge authorizations, never supplied by a client. */ authorizations: readonly CoalitionFundingAuthorization[]; pledges: readonly CoalitionPledge[]; contributions: readonly CoalitionContribution[]; }>; export type CoalitionFundingQuote = { purpose: 'bid' | 'shipment'; payer: string; cost: number; own: number; contributions: { donor: string; amount: number }[]; }; export class CoalitionFundingError extends Error { constructor(message: string) { super(message); this.name = 'CoalitionFundingError'; } } function requireFunding(condition: unknown, message: string): asserts condition { if (!condition) throw new CoalitionFundingError(message); } const record = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value); const amount = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) >= 0; const pairKey = (donor: string, recipient: string) => JSON.stringify([donor, recipient]); const edgeKey = (a: string, b: string) => JSON.stringify([a, b].sort()); const epoch = (value: unknown): value is string => typeof value === 'string' && value.length > 0 && value.length <= 256 && value.trim() === value; /** The engine must mint a globally fresh, never-reused epoch on edge formation, * preserve it while that edge remains allied, and revoke it atomically on a * break. A re-formed edge must receive a different epoch even in the same turn. * Legacy pairs need an issued epoch before funding may use this API. */ function fundingEdges(input: Pick) { const groups = coalitionGroups(input.seats, input.coalitionState); requireFunding(Array.isArray(input.authorizations), 'Supply current authoritative alliance funding epochs.'); const expected = new Set(); for (const group of groups) for (const edge of group.edges) expected.add(edgeKey(edge.members[0], edge.members[1])); const current = new Map(), usedEpochs = new Set(); for (const authorization of input.authorizations) { requireFunding(record(authorization) && Array.isArray(authorization.members) && authorization.members.length === 2 && typeof authorization.members[0] === 'string' && typeof authorization.members[1] === 'string' && authorization.members[0] < authorization.members[1] && epoch(authorization.epoch), 'Each funding epoch needs an ordered pair of allies.'); const key = edgeKey(authorization.members[0], authorization.members[1]); requireFunding(expected.has(key) && !current.has(key) && !usedEpochs.has(authorization.epoch), 'Funding epochs must uniquely match current alliance edges exactly.'); current.set(key, authorization.epoch); usedEpochs.add(authorization.epoch); } requireFunding(current.size === expected.size, 'Every current alliance edge needs an authoritative funding epoch.'); return current; } /** Separate stale escrow for refund after a break or epoch replacement. * The engine must resolve or cancel any affected committed payments and * atomically refund every returned stale record before using the retained * ledger. No wallet or pledge is changed by this pure function. */ export function reconcileCoalitionPledges(input: Pick): { pledges: CoalitionPledge[]; refunds: CoalitionPledge[]; } { requireFunding(record(input) && Array.isArray(input.pledges), 'Supply the complete pledged escrow ledger.'); const current = fundingEdges(input); const seated = new Set(input.seats.map(seat => seat.id)); const seen = new Set(), pledges: CoalitionPledge[] = [], refunds: CoalitionPledge[] = []; for (const pledge of input.pledges) { requireFunding(record(pledge) && typeof pledge.donor === 'string' && typeof pledge.recipient === 'string' && pledge.donor !== pledge.recipient && seated.has(pledge.donor) && seated.has(pledge.recipient) && amount(pledge.amount) && epoch(pledge.epoch), 'Every pledge needs seated parties, safe escrow and an epoch.'); const key = pairKey(pledge.donor, pledge.recipient); requireFunding(!seen.has(key), 'Each donor may have only one pledge for a recipient.'); seen.add(key); (current.get(edgeKey(pledge.donor, pledge.recipient)) === pledge.epoch ? pledges : refunds).push({ donor: pledge.donor, recipient: pledge.recipient, amount: pledge.amount, epoch: pledge.epoch, }); } return { pledges, refunds }; } /** Validate an exact declared split against current coalition membership and * each recipient-specific escrow. The group-funding and allocation procedure * is an online interpretation; the printed rule permits allies to help pay. * This is not an authorization, reservation, or a promise of future funds. */ export function quoteCoalitionFunding(input: CoalitionFundingInput): CoalitionFundingQuote { requireFunding(record(input) && (input.purpose === 'bid' || input.purpose === 'shipment') && amount(input.cost) && amount(input.ownAvailable) && Array.isArray(input.pledges) && Array.isArray(input.contributions), 'Choose a valid bid or shipment and safe spice amounts.'); requireFunding(typeof input.payer === 'string' && input.seats.some(seat => seat.id === input.payer), 'Choose a seated payer.'); const reconciled = reconcileCoalitionPledges(input); requireFunding(reconciled.refunds.length === 0, 'Reconcile and refund stale pledge escrow before payment.'); const pledges = new Map(); for (const pledge of reconciled.pledges) pledges.set(pairKey(pledge.donor, pledge.recipient), pledge); const donors = new Set(); const contributions: { donor: string; amount: number }[] = []; let own = input.cost; for (const share of input.contributions) { requireFunding(record(share) && typeof share.donor === 'string' && amount(share.amount) && share.amount > 0 && !donors.has(share.donor), 'Declare each donating ally once with a positive safe amount.'); const pledge = pledges.get(pairKey(share.donor, input.payer)); requireFunding(pledge && share.amount <= pledge.amount, 'Each contribution needs enough current escrow pledged to this payer.'); requireFunding(share.amount <= own, 'Contributions cannot exceed the cost.'); own -= share.amount; donors.add(share.donor); contributions.push({ donor: share.donor, amount: share.amount }); } requireFunding(own <= input.ownAvailable, 'The payer cannot cover the remaining cost.'); return { purpose: input.purpose, payer: input.payer, cost: input.cost, own, contributions }; } /** Revalidate the complete current input at the moment of payment and return * a new pledge ledger. The engine must debit quote.own and install this ledger * in the same authoritative action; recipient routing and replay protection * remain native transaction responsibilities. Never apply a stale quote alone. */ export function settleCoalitionFunding(input: CoalitionFundingInput): { quote: CoalitionFundingQuote; pledges: CoalitionPledge[]; } { const quote = quoteCoalitionFunding(input); const debits = new Map(quote.contributions.map(share => [share.donor, share.amount])); const pledges = input.pledges.map(pledge => ({ ...pledge, amount: pledge.amount - (pledge.recipient === quote.payer ? debits.get(pledge.donor) ?? 0 : 0), })); return { quote, pledges }; }