UNPKG

lotus-sdk

Version:

Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem

241 lines (240 loc) 8.73 kB
import { randomBytes } from 'node:crypto'; import { SwapPhase as Phase, DEFAULT_BURN_CONFIG } from './types.js'; export class SwapPoolManager { pools; constructor() { this.pools = new Map(); } createPool(creatorPeerId, params) { const poolId = this._generatePoolId(); const burnPercentage = params.burnPercentage ?? DEFAULT_BURN_CONFIG.burnPercentage; if (burnPercentage < 0.0005 || burnPercentage > 0.01) { throw new Error('Burn percentage must be between 0.05% and 1.0%'); } const pool = { poolId, creatorPeerId, denomination: params.denomination, minParticipants: params.minParticipants ?? 3, maxParticipants: params.maxParticipants ?? 10, feeRate: params.feeRate ?? 1, feePerParticipant: this._estimateFeePerParticipant(params.feeRate ?? 1), burnConfig: { ...DEFAULT_BURN_CONFIG, burnPercentage, }, participants: [], participantMap: new Map(), outputGroups: [], sharedOutputs: [], settlementMapping: new Map(), setupTransactions: [], settlementTransactions: [], settlementSessions: new Map(), phase: Phase.DISCOVERY, createdAt: Date.now(), setupTimeout: params.setupTimeout ?? 600000, settlementTimeout: params.settlementTimeout ?? 600000, aborted: false, }; this.pools.set(poolId, pool); return poolId; } addParticipant(poolId, peerId, publicKey, input, ownershipProof, finalOutputEncrypted, finalOutputCommitment) { const pool = this.getPool(poolId); if (!pool) { throw new Error(`Pool ${poolId} not found`); } if (pool.phase !== Phase.DISCOVERY && pool.phase !== Phase.REGISTRATION) { throw new Error(`Cannot join pool in phase ${pool.phase}`); } if (pool.participantMap.has(peerId)) { throw new Error(`Peer ${peerId} already registered`); } if (pool.participants.length >= pool.maxParticipants) { throw new Error(`Pool full (${pool.maxParticipants} max)`); } if (input.amount !== pool.denomination) { throw new Error(`Input amount ${input.amount} does not match denomination ${pool.denomination}`); } const participant = { peerId, participantIndex: pool.participants.length, publicKey, input, ownershipProof, finalOutputEncrypted, finalOutputCommitment, setupConfirmed: false, joinedAt: Date.now(), }; pool.participants.push(participant); pool.participantMap.set(peerId, participant); if (pool.phase === Phase.DISCOVERY) { pool.phase = Phase.REGISTRATION; } return participant.participantIndex; } removeParticipant(poolId, peerId) { const pool = this.getPool(poolId); if (!pool) { throw new Error(`Pool ${poolId} not found`); } const participant = pool.participantMap.get(peerId); if (!participant) { return; } pool.participantMap.delete(peerId); const index = pool.participants.findIndex(p => p.peerId === peerId); if (index !== -1) { pool.participants.splice(index, 1); } pool.participants.forEach((p, i) => { p.participantIndex = i; }); } transitionPhase(poolId, newPhase) { const pool = this.getPool(poolId); if (!pool) { throw new Error(`Pool ${poolId} not found`); } const oldPhase = pool.phase; pool.phase = newPhase; if (newPhase === Phase.SETUP && !pool.startedAt) { pool.startedAt = Date.now(); } if (newPhase === Phase.COMPLETE && !pool.completedAt) { pool.completedAt = Date.now(); } } abortPool(poolId, reason) { const pool = this.getPool(poolId); if (!pool) { throw new Error(`Pool ${poolId} not found`); } pool.aborted = true; pool.abortReason = reason; pool.phase = Phase.ABORTED; } getPool(poolId) { return this.pools.get(poolId); } getAllPools() { return Array.from(this.pools.values()); } getPoolsByPhase(phase) { return Array.from(this.pools.values()).filter(pool => pool.phase === phase); } hasMinimumParticipants(poolId) { const pool = this.getPool(poolId); if (!pool) return false; return pool.participants.length >= pool.minParticipants; } allSetupsConfirmed(poolId) { const pool = this.getPool(poolId); if (!pool) return false; return pool.participants.every(p => p.setupConfirmed); } allDestinationsRevealed(poolId) { const pool = this.getPool(poolId); if (!pool) return false; return pool.participants.every(p => p.finalAddress !== undefined); } allSettlementsConfirmed(poolId) { const pool = this.getPool(poolId); if (!pool) return false; return pool.sharedOutputs.every(o => o.settlementConfirmed); } getPoolStats(poolId) { const pool = this.getPool(poolId); if (!pool) return undefined; const anonymitySet = this._calculateAnonymitySet(pool); const duration = pool.completedAt ? pool.completedAt - pool.createdAt : undefined; const setupDuration = pool.startedAt ? (pool.completedAt ?? Date.now()) - pool.startedAt : undefined; return { poolId: pool.poolId, phase: pool.phase, participants: pool.participants.length, denomination: pool.denomination, totalBurned: pool.participants.length * Math.floor(pool.denomination * pool.burnConfig.burnPercentage), totalFees: pool.participants.length * 2 * pool.feePerParticipant, anonymitySet, duration, setupDuration, }; } determineOptimalGroupSize(participantCount) { if (participantCount <= 9) { return { groupSize: 2, groupCount: Math.floor(participantCount / 2), anonymityPerGroup: 2, reasoning: `Small pool (${participantCount} participants): 2-of-2 optimal for simplicity. Total anonymity: ${participantCount}! = ${this._factorial(participantCount)}`, recommendedRounds: 2, }; } if (participantCount <= 14) { return { groupSize: 3, groupCount: Math.floor(participantCount / 3), anonymityPerGroup: 6, reasoning: `Medium-small pool (${participantCount} participants): 3-of-3 provides 6 mappings per group`, recommendedRounds: 1, }; } if (participantCount <= 49) { return { groupSize: 5, groupCount: Math.floor(participantCount / 5), anonymityPerGroup: 120, reasoning: `Medium pool (${participantCount} participants): 5-of-5 provides 120 mappings per group (excellent anonymity)`, recommendedRounds: 1, }; } return { groupSize: 10, groupCount: Math.floor(participantCount / 10), anonymityPerGroup: 3628800, reasoning: `Large pool (${participantCount} participants): 10-of-10 necessary for large-scale coordination`, recommendedRounds: 1, }; } removePool(poolId) { this.pools.delete(poolId); } _generatePoolId() { return randomBytes(32).toString('hex'); } _estimateFeePerParticipant(feeRate) { const avgTxSize = 225; return Math.ceil(avgTxSize * feeRate); } _calculateAnonymitySet(pool) { const n = pool.participants.length; if (pool.groupSizeStrategy) { const groupSize = pool.groupSizeStrategy.groupSize; const numGroups = Math.floor(n / groupSize); return Math.pow(this._factorial(groupSize), numGroups); } return this._factorial(n); } _factorial(n) { if (n <= 1) return 1; let result = 1; for (let i = 2; i <= n; i++) { result *= i; } return result; } }