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
JavaScript
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;
}
}