import { coalitionGroups, type CoalitionSeat, type CoalitionState } from './coalition-alliance'; import { CoalitionFundingError, reconcileCoalitionPledges, type CoalitionFundingAuthorization, type CoalitionPledge, } from './coalition-funding'; /** Epochs are never reused, including after an edge leaves and reforms. */ export type CoalitionEscrowState = Readonly<{ authorizations: readonly CoalitionFundingAuthorization[]; pledges: readonly CoalitionPledge[]; usedEpochs: readonly string[]; }>; export type CoalitionEscrowMembership = Readonly<{ seats: readonly CoalitionSeat[]; coalitionState?: CoalitionState; }>; export type CoalitionEscrowRefund = Readonly<{ donor: string; amount: number }>; const whole = (n: unknown): n is number => Number.isSafeInteger(n) && (n as number) >= 0; const validEpoch = (s: unknown): s is string => typeof s === 'string' && !!s && s.length <= 256 && s.trim() === s; function check(condition: unknown, message: string): asserts condition { if (!condition) throw new CoalitionFundingError(message); } const key = (members: readonly string[]) => JSON.stringify(members); const edge = (a: string, b: string) => key([a, b].sort()); const directed = (donor: string, recipient: string) => key([donor, recipient]); function checkedSum(a: number, b: number): number { const result = a + b; check(whole(result), 'Escrow totals must be safe integers.'); return result; } function edges(membership: CoalitionEscrowMembership): Map { const out = new Map(); for (const group of coalitionGroups(membership.seats, membership.coalitionState)) for (const item of group.edges) out.set(key(item.members), item.members); return out; } function validateHistory(escrow: CoalitionEscrowState) { check(!!escrow && Array.isArray(escrow.authorizations) && Array.isArray(escrow.pledges) && Array.isArray(escrow.usedEpochs) && escrow.usedEpochs.every(validEpoch) && new Set(escrow.usedEpochs).size === escrow.usedEpochs.length, 'The escrow must retain unique valid epoch history.'); const used = new Set(escrow.usedEpochs); check(escrow.authorizations.every(auth => auth && validEpoch(auth.epoch) && used.has(auth.epoch)) && escrow.pledges.every(pledge => pledge && validEpoch(pledge.epoch) && used.has(pledge.epoch)), 'Every escrow epoch must appear in its retained history.'); } function validateCurrent(membership: CoalitionEscrowMembership, escrow: CoalitionEscrowState) { validateHistory(escrow); const result = reconcileCoalitionPledges({ ...membership, authorizations: escrow.authorizations, pledges: escrow.pledges }); check(result.refunds.length === 0, 'Reconcile stale pledges before changing escrow.'); } /** * Apply one authoritative alliance transition. Call once for each formation or * break, not on a collapsed before/after history: an edge broken and reformed * must receive a new epoch. The caller supplies freshly generated IDs for only * the new edges, atomically installs the result and credits each donor refund. * A pledge with a live commitment cannot be refunded until that commitment * has been settled or canceled. The caller supplies every such locked pledge. */ export function reconcileCoalitionEscrow(input: Readonly<{ before: CoalitionEscrowMembership; after: CoalitionEscrowMembership; escrow: CoalitionEscrowState; newAuthorizations: readonly CoalitionFundingAuthorization[]; locked?: readonly CoalitionPledge[]; }>): { escrow: CoalitionEscrowState; refunds: CoalitionEscrowRefund[] } { const { before, after, escrow } = input; validateCurrent(before, escrow); const oldEdges = edges(before), newEdges = edges(after); const beforeIds = before.seats.map(seat => seat.id).sort(); const afterIds = after.seats.map(seat => seat.id).sort(); check(key(beforeIds) === key(afterIds), 'Escrow transitions require the same seated players.'); check(Array.isArray(input.newAuthorizations) && Array.isArray(input.locked ?? []), 'Supply new edge authorizations and any locked pledges.'); const previous = new Map(escrow.authorizations.map(auth => [key(auth.members), auth])); const history = new Set(escrow.usedEpochs); const fresh = new Map(); for (const auth of input.newAuthorizations) { check(!!auth && Array.isArray(auth.members) && auth.members.length === 2 && typeof auth.members[0] === 'string' && typeof auth.members[1] === 'string' && auth.members[0] < auth.members[1] && validEpoch(auth.epoch), 'Each fresh epoch needs an ordered pair of allies.'); const id = key(auth.members); check(newEdges.has(id) && !oldEdges.has(id) && !fresh.has(id) && !history.has(auth.epoch), 'Fresh epochs must uniquely authorize new edges and cannot be reused.'); history.add(auth.epoch); fresh.set(id, { members: [auth.members[0], auth.members[1]], epoch: auth.epoch }); } const authorizations: CoalitionFundingAuthorization[] = []; for (const [id, members] of newEdges) { const auth = oldEdges.has(id) ? previous.get(id) : fresh.get(id); check(auth, 'Every new alliance edge needs a fresh epoch.'); authorizations.push({ members: [members[0], members[1]], epoch: auth.epoch }); } const pledged = new Map(escrow.pledges.map(item => [directed(item.donor, item.recipient), item])); const locked = new Set(); for (const item of input.locked ?? []) { check(!!item && typeof item.donor === 'string' && typeof item.recipient === 'string' && whole(item.amount) && item.amount > 0 && validEpoch(item.epoch), 'A locked pledge needs a valid committed amount.'); const id = directed(item.donor, item.recipient), pledge = pledged.get(id); check(!locked.has(id) && pledge && pledge.epoch === item.epoch && item.amount <= pledge.amount, 'Each committed pledge must match its available escrow once.'); locked.add(id); check(newEdges.has(edge(item.donor, item.recipient)), 'Resolve committed payments before breaking their alliance edge.'); } const reconciled = reconcileCoalitionPledges({ ...after, authorizations, pledges: escrow.pledges }); const totals = new Map(); for (const item of reconciled.refunds) totals.set(item.donor, checkedSum(totals.get(item.donor) ?? 0, item.amount)); return { escrow: { authorizations, pledges: reconciled.pledges, usedEpochs: [...history] }, refunds: [...totals].map(([donor, amount]) => ({ donor, amount })), }; } /** Return a signed wallet change and replacement ledger without mutating the * donor. `available` is uncommitted wallet spice; `locked` is the already * committed amount of this recipient's existing pledge. The engine must * verify the actual wallet and atomically apply both outputs. */ export function setCoalitionPledge(input: CoalitionEscrowMembership & Readonly<{ escrow: CoalitionEscrowState; donor: string; recipient: string; amount: number; available: number; locked?: number; }>): { escrow: CoalitionEscrowState; walletDelta: number } { const { escrow, donor, recipient, amount, available } = input; const locked = input.locked ?? 0; validateCurrent(input, escrow); check(typeof donor === 'string' && typeof recipient === 'string' && donor !== recipient && whole(amount) && whole(available) && whole(locked), 'Choose a valid donor, recipient and safe spice amounts.'); const auth = escrow.authorizations.find(item => key(item.members) === edge(donor, recipient)); check(auth, 'A pledge requires a current authorized alliance edge.'); const old = escrow.pledges.find(item => item.donor === donor && item.recipient === recipient); const prior = old?.amount ?? 0; check(locked <= prior && amount >= locked, 'Committed pledge spice cannot be withdrawn.'); const delta = prior - amount; check(delta >= 0 || -delta <= available, 'The donor lacks uncommitted spice for this pledge.'); if (delta > 0) checkedSum(available, delta); const pledges = escrow.pledges.filter(item => item.donor !== donor || item.recipient !== recipient) .map(item => ({ ...item })); if (amount) pledges.push({ donor, recipient, amount, epoch: auth.epoch }); return { escrow: { authorizations: escrow.authorizations.map(item => ({ members: [item.members[0], item.members[1]], epoch: item.epoch, })), pledges, usedEpochs: [...escrow.usedEpochs] }, walletDelta: delta }; }