lotus-sdk
Version:
Central repository for several classes of tools for integrating with, and building for, the Lotusia ecosystem
814 lines (813 loc) • 38.8 kB
JavaScript
import { Hash } from '../../bitcore/crypto/hash.js';
import { Schnorr } from '../../bitcore/crypto/schnorr.js';
import { Signature } from '../../bitcore/crypto/signature.js';
import { Address } from '../../bitcore/address.js';
import { Transaction } from '../../bitcore/transaction/index.js';
import { Script } from '../../bitcore/script.js';
import { createMuSigTaprootAddress } from '../../bitcore/taproot/musig2.js';
import { MuSig2P2PCoordinator } from '../musig2/coordinator.js';
import { MuSig2Event, TransactionType } from '../musig2/types.js';
import { SwapPoolManager } from './pool.js';
import { SwapSigBurnMechanism } from './burn.js';
import { SwapSigP2PProtocolHandler } from './protocol-handler.js';
import { TransactionMonitor } from '../blockchain-utils.js';
import { SwapPhase, SwapSigEvent } from './types.js';
export class SwapSigCoordinator extends MuSig2P2PCoordinator {
swapConfig;
poolManager;
burnMechanism;
privateKey;
swapSigProtocolHandler;
txMonitor;
constructor(privateKey, p2pConfig, musig2Config, swapSigConfig) {
super(p2pConfig, musig2Config);
this.swapConfig = {
minParticipants: 3,
maxParticipants: 10,
feeRate: 1,
setupTimeout: 600000,
settlementTimeout: 600000,
requireEncryptedDestinations: true,
randomizeOutputOrder: true,
chronikUrl: 'https://chronik.lotusia.org',
requiredConfirmations: 1,
confirmationPollInterval: 3000,
...swapSigConfig,
};
this.privateKey = privateKey;
this.poolManager = new SwapPoolManager();
this.burnMechanism = new SwapSigBurnMechanism();
this.txMonitor = new TransactionMonitor(this.swapConfig.chronikUrl);
this.swapSigProtocolHandler = new SwapSigP2PProtocolHandler();
this.swapSigProtocolHandler.setCoordinator(this);
this.registerProtocol(this.swapSigProtocolHandler);
this._setupSwapSigEventHandlers();
}
async start() {
await super.start();
}
async stop() {
const pools = this.poolManager.getAllPools();
for (const pool of pools) {
if (pool.phase !== SwapPhase.COMPLETE &&
pool.phase !== SwapPhase.ABORTED) {
this.poolManager.abortPool(pool.poolId, 'Coordinator stopped');
this.emit(SwapSigEvent.POOL_ABORTED, pool.poolId, 'Coordinator stopped');
}
}
await super.stop();
}
async discoverPools(filters) {
const localPools = this.poolManager
.getAllPools()
.filter(pool => pool.phase === SwapPhase.DISCOVERY ||
pool.phase === SwapPhase.REGISTRATION)
.map(pool => this._createPoolAnnouncement(pool));
const filtered = localPools.filter(announcement => {
if (filters?.denomination &&
announcement.denomination !== filters.denomination) {
return false;
}
if (filters?.minParticipants &&
announcement.currentParticipants < filters.minParticipants) {
return false;
}
if (filters?.maxParticipants &&
announcement.maxParticipants > filters.maxParticipants) {
return false;
}
if (!filters?.denomination &&
this.swapConfig.preferredDenominations &&
this.swapConfig.preferredDenominations.length > 0) {
return this.swapConfig.preferredDenominations.includes(announcement.denomination);
}
return true;
});
return filtered;
}
async createPool(params) {
const poolParams = {
denomination: params.denomination,
minParticipants: params.minParticipants ?? this.swapConfig.minParticipants,
maxParticipants: params.maxParticipants ?? this.swapConfig.maxParticipants,
feeRate: params.feeRate ?? this.swapConfig.feeRate,
burnPercentage: params.burnPercentage,
setupTimeout: params.setupTimeout ?? this.swapConfig.setupTimeout,
settlementTimeout: params.settlementTimeout ?? this.swapConfig.settlementTimeout,
};
const poolId = this.poolManager.createPool(this.libp2pNode.peerId.toString(), poolParams);
const pool = this.poolManager.getPool(poolId);
if (!pool) {
throw new Error('Failed to create pool');
}
await this._announcePool(pool);
this.emit(SwapSigEvent.POOL_CREATED, pool);
return poolId;
}
async joinPool(poolId, input, finalDestination) {
let pool = this.poolManager.getPool(poolId);
if (!pool) {
console.log(`[SwapSig] Pool ${poolId.substring(0, 8)}... not found locally, discovering from DHT...`);
const announcement = await this.discoverResource('swapsig-pool', poolId, 5000);
if (!announcement) {
throw new Error(`Pool ${poolId} not found locally or in DHT`);
}
const poolData = announcement.data;
const localPoolId = this.poolManager.createPool(poolData.creatorPeerId, {
denomination: poolData.denomination,
minParticipants: poolData.minParticipants,
maxParticipants: poolData.maxParticipants,
burnPercentage: poolData.burnConfig.burnPercentage,
setupTimeout: poolData.setupTimeout,
settlementTimeout: poolData.settlementTimeout,
});
pool = this.poolManager.getPool(localPoolId);
if (pool) {
this.poolManager['pools'].delete(localPoolId);
pool.poolId = poolId;
this.poolManager['pools'].set(poolId, pool);
}
console.log(`[SwapSig] Pool ${poolId.substring(0, 8)}... discovered and imported`);
}
if (!pool) {
throw new Error('Failed to get or create pool');
}
if (input.satoshis !== pool.denomination) {
throw new Error(`Input amount ${input.satoshis} does not match denomination ${pool.denomination}`);
}
const ownershipMessage = Buffer.concat([
Buffer.from(poolId, 'hex'),
Buffer.from(input.txId, 'hex'),
Buffer.from([input.outputIndex]),
]);
const ownershipHash = Hash.sha256(ownershipMessage);
const ownershipProof = Schnorr.sign(ownershipHash, this.privateKey).toBuffer();
const finalOutputEncrypted = this._encryptAddress(finalDestination, poolId);
const finalOutputCommitment = Hash.sha256(finalOutputEncrypted);
const participantInput = {
txId: input.txId,
outputIndex: input.outputIndex,
amount: input.satoshis,
script: input.script,
address: input.address ||
Address.fromPublicKeyHash(Hash.sha256ripemd160(input.script.toBuffer())),
};
const participantIndex = this.poolManager.addParticipant(poolId, this.libp2pNode.peerId.toString(), this.privateKey.publicKey, participantInput, ownershipProof, finalOutputEncrypted, finalOutputCommitment);
await this._advertiseSwapSigner(pool);
await this._broadcastParticipantRegistered(pool, participantIndex);
this.emit(SwapSigEvent.POOL_JOINED, poolId, participantIndex);
return participantIndex;
}
async executeSwap(poolId, input, finalDestination) {
await this.joinPool(poolId, input, finalDestination);
await this._waitForMinimumParticipants(poolId);
await this._executeSetupRound(poolId);
await this._waitForSetupConfirmations(poolId);
await this._revealFinalDestinations(poolId);
await this._executeSettlementRound(poolId);
await this._waitForSettlementConfirmations(poolId);
const pool = this.poolManager.getPool(poolId);
if (!pool) {
throw new Error('Pool not found');
}
const myParticipant = this._getMyParticipant(pool);
const settlementInfo = pool.settlementMapping.get(myParticipant.participantIndex);
if (!settlementInfo?.txId) {
throw new Error('Settlement transaction not found');
}
return settlementInfo.txId;
}
getPoolStats(poolId) {
return this.poolManager.getPoolStats(poolId);
}
getActivePools() {
return this.poolManager.getAllPools();
}
getPoolManager() {
return this.poolManager;
}
getBurnMechanism() {
return this.burnMechanism;
}
getSwapConfig() {
return this.swapConfig;
}
_setupSwapSigEventHandlers() {
super.on(MuSig2Event.SIGNING_REQUEST_RECEIVED, async (request) => {
try {
const metadata = request.metadata;
if (metadata?.transactionType !== TransactionType.SWAP) {
return;
}
if (metadata?.swapPhase !== SwapPhase.SETTLEMENT) {
return;
}
const myPubKey = this.privateKey.publicKey.toString();
const isRequiredSigner = request.requiredPublicKeys.some((pk) => pk.toString() === myPubKey);
if (!isRequiredSigner) {
return;
}
const poolId = metadata.swapPoolId;
if (!poolId)
return;
const pool = this.poolManager.getPool(poolId);
if (!pool) {
console.log(`[SwapSig] Received signing request for unknown pool ${poolId.substring(0, 8)}...`);
return;
}
console.log(`[SwapSig] Discovered signing request ${request.requestId.substring(0, 8)}... for pool ${poolId.substring(0, 8)}...`);
await this.joinSigningRequest(request.requestId, this.privateKey);
console.log(`[SwapSig] Auto-joined signing request ${request.requestId.substring(0, 8)}...`);
this.emit(SwapSigEvent.SWAPSIG_REQUEST_JOINED, request.requestId, poolId);
}
catch (error) {
console.error('[SwapSig] Error handling signing request discovery:', error);
}
});
super.on(MuSig2Event.SESSION_READY, ({ sessionId, requestId }) => {
console.log(`[SwapSig] Session ${sessionId.substring(0, 8)}... ready for signing (all participants joined)`);
this.emit(SwapSigEvent.SWAPSIG_SESSION_READY, sessionId, requestId);
});
super.on(MuSig2Event.SESSION_COMPLETE, sessionId => {
console.log(`[SwapSig] Session ${sessionId.substring(0, 8)}... completed`);
this.emit(SwapSigEvent.SWAPSIG_SESSION_COMPLETE, sessionId);
});
}
async _announcePool(pool) {
const announcement = this._createPoolAnnouncement(pool);
await this.announceResource('swapsig-pool', pool.poolId, announcement, {
ttl: pool.setupTimeout + pool.settlementTimeout,
});
await this._broadcastMessage({
type: 'swapsig:pool-announce',
poolId: pool.poolId,
from: this.libp2pNode.peerId.toString(),
payload: announcement,
timestamp: Date.now(),
messageId: this._generateMessageId(),
});
}
_createPoolAnnouncement(pool) {
const announcementData = Buffer.concat([
Buffer.from(pool.poolId, 'hex'),
Buffer.from(pool.denomination.toString()),
Buffer.from(pool.createdAt.toString()),
]);
const announcementHash = Hash.sha256(announcementData);
const signature = Schnorr.sign(announcementHash, this.privateKey);
return {
poolId: pool.poolId,
denomination: pool.denomination,
minParticipants: pool.minParticipants,
maxParticipants: pool.maxParticipants,
currentParticipants: pool.participants.length,
burnConfig: pool.burnConfig,
createdAt: pool.createdAt,
expiresAt: pool.createdAt + pool.setupTimeout + pool.settlementTimeout,
setupTimeout: pool.setupTimeout,
settlementTimeout: pool.settlementTimeout,
creatorPeerId: pool.creatorPeerId,
creatorSignature: signature.toBuffer(),
};
}
async _advertiseSwapSigner(pool) {
await this.advertiseSigner(this.privateKey, {
transactionTypes: [TransactionType.SWAP],
minAmount: pool.denomination,
maxAmount: pool.denomination,
}, {
ttl: pool.setupTimeout + pool.settlementTimeout,
metadata: {
description: `SwapSig signer for pool ${pool.poolId}`,
fees: 0,
},
});
}
async _broadcastParticipantRegistered(pool, participantIndex) {
await this._broadcastMessage({
type: 'swapsig:participant-registered',
poolId: pool.poolId,
from: this.libp2pNode.peerId.toString(),
payload: { participantIndex },
timestamp: Date.now(),
messageId: this._generateMessageId(),
});
}
async _broadcastMessage(message) {
await this.broadcast({
type: message.type,
from: message.from,
payload: message.payload,
timestamp: message.timestamp,
messageId: message.messageId,
protocol: 'swapsig',
});
}
async _waitForMinimumParticipants(poolId) {
const pool = this.poolManager.getPool(poolId);
if (!pool) {
throw new Error('Pool not found');
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timeout waiting for minimum participants'));
}, pool.setupTimeout);
const checkMinimum = () => {
if (this.poolManager.hasMinimumParticipants(poolId)) {
clearTimeout(timeout);
resolve();
}
else {
setTimeout(checkMinimum, 1000);
}
};
checkMinimum();
});
}
_computeOutputGroups(participants, groupSize) {
const groups = [];
const n = participants.length;
if (groupSize === 2) {
for (let i = 0; i < n; i++) {
const partner = (i + 1) % n;
groups.push([i, partner]);
}
}
else {
const numCompleteGroups = Math.floor(n / groupSize);
for (let g = 0; g < numCompleteGroups; g++) {
const group = [];
for (let i = 0; i < groupSize; i++) {
group.push(g * groupSize + i);
}
groups.push(group);
}
const remaining = n % groupSize;
if (remaining > 0) {
const lastGroup = [];
for (let i = 0; i < remaining; i++) {
lastGroup.push(numCompleteGroups * groupSize + i);
}
while (lastGroup.length < groupSize) {
lastGroup.push(lastGroup.length % n);
}
groups.push(lastGroup);
}
}
return groups;
}
_computeSettlementMapping(pool) {
const mapping = new Map();
const n = pool.participants.length;
const numGroups = pool.outputGroups.length;
for (let g = 0; g < numGroups; g++) {
const sourceOutput = pool.sharedOutputs[g];
let receiverIndex;
if (!pool.groupSizeStrategy) {
throw new Error('Pool group size strategy not initialized');
}
if (pool.groupSizeStrategy.groupSize === 2) {
receiverIndex = (g + 1) % n;
}
else {
const nextGroup = (g + 1) % numGroups;
receiverIndex = pool.outputGroups[nextGroup][0];
}
const receiver = pool.participants[receiverIndex];
if (!receiver.finalAddress) {
throw new Error(`Receiver ${receiverIndex} has not revealed final address`);
}
mapping.set(receiverIndex, {
receiverIndex,
sourceOutputIndex: g,
sourceOutput,
finalDestination: receiver.finalAddress,
signers: sourceOutput.signers,
confirmed: false,
});
}
return mapping;
}
async _executeSetupRound(poolId) {
const pool = this.poolManager.getPool(poolId);
if (!pool) {
throw new Error(`Pool ${poolId} not found`);
}
if (pool.phase !== SwapPhase.REGISTRATION) {
throw new Error(`Cannot execute setup round: pool is in ${pool.phase} phase`);
}
console.log(`[SwapSig] Executing setup round for pool ${poolId.substring(0, 8)}...`);
const groupStrategy = this.poolManager.determineOptimalGroupSize(pool.participants.length);
console.log(`[SwapSig] Group strategy: ${groupStrategy.reasoning}`);
const outputGroups = this._computeOutputGroups(pool.participants, groupStrategy.groupSize);
console.log(`[SwapSig] Created ${outputGroups.length} output groups`);
pool.groupSizeStrategy = groupStrategy;
pool.outputGroups = outputGroups;
pool.sharedOutputs = [];
for (let i = 0; i < outputGroups.length; i++) {
const group = outputGroups[i];
const signerPubKeys = group.map(idx => pool.participants[idx].publicKey);
const { address, keyAggContext, commitment } = createMuSigTaprootAddress(signerPubKeys, 'livenet');
console.log(`[SwapSig] Group ${i}: ${group.length}-of-${group.length} MuSig2 address: ${address.toString()}`);
pool.sharedOutputs.push({
outputIndex: i,
signers: signerPubKeys,
aggregatedKey: keyAggContext.aggregatedPubKey,
taprootAddress: address,
amount: pool.denomination,
txId: '',
confirmed: false,
});
}
const myParticipant = this._getMyParticipant(pool);
const myGroupIndex = outputGroups.findIndex(group => group.includes(myParticipant.participantIndex));
if (myGroupIndex === -1) {
throw new Error('My participant not found in any group');
}
const mySharedOutput = pool.sharedOutputs[myGroupIndex];
const setupTx = await this._buildSetupTransaction(myParticipant, mySharedOutput.taprootAddress, pool.denomination, poolId);
setupTx.sign(this.privateKey);
const txHex = setupTx.toString();
const txId = await this.txMonitor.broadcastTransaction(txHex);
if (!txId) {
throw new Error('Failed to broadcast setup transaction');
}
console.log(`[SwapSig] Setup transaction broadcast: ${txId.substring(0, 8)}...`);
myParticipant.setupTxId = txId;
mySharedOutput.txId = txId;
this.poolManager.transitionPhase(poolId, SwapPhase.SETUP_CONFIRM);
console.log(`[SwapSig] Setup round complete, waiting for confirmations...`);
}
async _buildSetupTransaction(participant, sharedAddress, amount, poolId) {
const burnAmount = this.burnMechanism.calculateBurnAmount(amount);
const tx = new Transaction();
tx.from(participant.input);
tx.to(sharedAddress, amount);
const burnOutput = this.burnMechanism.createBurnOutput(burnAmount, poolId);
tx.addOutput(burnOutput);
const feeRate = this.swapConfig.feeRate || 1;
tx.feePerByte(feeRate);
tx.change(participant.input.address);
return tx;
}
async _waitForSetupConfirmations(poolId) {
const pool = this.poolManager.getPool(poolId);
if (!pool) {
throw new Error(`Pool ${poolId} not found`);
}
console.log(`[SwapSig] Waiting for setup confirmations for pool ${poolId.substring(0, 8)}...`);
const setupTxIds = [];
for (const participant of pool.participants) {
if (participant.setupTxId) {
setupTxIds.push(participant.setupTxId);
}
}
if (setupTxIds.length !== pool.participants.length) {
console.warn(`[SwapSig] Not all participants have broadcast setup transactions: ${setupTxIds.length}/${pool.participants.length}`);
}
const requiredConfs = this.swapConfig.requiredConfirmations || 1;
const pollInterval = this.swapConfig.confirmationPollInterval || 3000;
console.log(`[SwapSig] Waiting for ${requiredConfs} confirmations on ${setupTxIds.length} transactions...`);
const confirmationPromises = setupTxIds.map(txId => this.txMonitor.waitForConfirmations(txId, requiredConfs, pollInterval, this.swapConfig.setupTimeout));
const results = await Promise.all(confirmationPromises);
const allConfirmed = results.every(result => result?.isConfirmed);
if (!allConfirmed) {
console.error(`[SwapSig] Setup confirmation timeout for pool ${poolId.substring(0, 8)}...`);
this.poolManager.abortPool(poolId, 'Setup confirmation timeout');
this.emit(SwapSigEvent.POOL_ABORTED, poolId, 'Setup confirmation timeout');
throw new Error('Setup confirmation timeout');
}
console.log(`[SwapSig] All setup transactions confirmed!`);
for (const participant of pool.participants) {
participant.setupConfirmed = true;
}
for (const output of pool.sharedOutputs) {
output.confirmed = true;
}
this.poolManager.transitionPhase(poolId, SwapPhase.REVEAL);
this.emit(SwapSigEvent.SETUP_COMPLETE, poolId);
}
async _revealFinalDestinations(poolId) {
const pool = this.poolManager.getPool(poolId);
if (!pool) {
throw new Error(`Pool ${poolId} not found`);
}
console.log(`[SwapSig] Revealing final destinations for pool ${poolId.substring(0, 8)}...`);
if (!this.swapConfig.requireEncryptedDestinations) {
console.log(`[SwapSig] Encryption disabled, addresses already plaintext`);
}
else {
console.log(`[SwapSig] Destination reveal/decryption not yet implemented (proceeding with test addresses)`);
}
this.poolManager.transitionPhase(poolId, SwapPhase.SETTLEMENT);
this.emit(SwapSigEvent.REVEAL_COMPLETE, poolId);
}
async _executeSettlementRound(poolId) {
const pool = this.poolManager.getPool(poolId);
if (!pool) {
throw new Error(`Pool ${poolId} not found`);
}
if (pool.phase !== SwapPhase.SETTLEMENT) {
throw new Error(`Cannot execute settlement round: pool is in ${pool.phase} phase`);
}
console.log(`[SwapSig] Executing settlement round for pool ${poolId.substring(0, 8)}...`);
const settlementMapping = this._computeSettlementMapping(pool);
console.log(`[SwapSig] Computed settlement mapping for ${settlementMapping.size} participants`);
pool.settlementMapping = settlementMapping;
const settlementTxs = [];
for (const [receiverIndex, settlementInfo] of settlementMapping) {
const tx = await this._buildSettlementTransaction(settlementInfo.sourceOutput, settlementInfo.finalDestination, pool);
settlementTxs.push({
tx,
outputIndex: settlementInfo.sourceOutputIndex,
});
console.log(`[SwapSig] Built settlement tx for output ${settlementInfo.sourceOutputIndex} → receiver ${receiverIndex}`);
}
console.log(`[SwapSig] Announcing ${settlementTxs.length} MuSig2 signing requests...`);
for (const { tx, outputIndex } of settlementTxs) {
const sharedOutput = pool.sharedOutputs[outputIndex];
const sigHashBuffer = Buffer.from(tx.id, 'hex');
const requestId = await this.announceSigningRequest(sharedOutput.signers, sigHashBuffer, this.privateKey, {
metadata: {
transactionType: TransactionType.SWAP,
swapPhase: SwapPhase.SETTLEMENT,
swapPoolId: poolId,
outputIndex,
transactionHex: tx.toString(),
taprootKeyPath: true,
},
});
console.log(`[SwapSig] Announced signing request ${requestId.substring(0, 8)}... for output ${outputIndex}`);
}
this.poolManager.transitionPhase(poolId, SwapPhase.SETTLEMENT_CONFIRM);
console.log(`[SwapSig] Settlement round announced, waiting for MuSig2 coordination and confirmations...`);
}
async _buildSettlementTransaction(sharedOutput, destination, pool) {
const tx = new Transaction();
tx.from({
txId: sharedOutput.txId,
outputIndex: sharedOutput.outputIndex,
satoshis: sharedOutput.amount,
script: Script.fromAddress(sharedOutput.taprootAddress),
});
const feeRate = this.swapConfig.feeRate || 1;
const outputAmount = sharedOutput.amount - feeRate * 200;
tx.to(destination, outputAmount);
tx.feePerByte(feeRate);
return tx;
}
async _waitForSettlementConfirmations(poolId) {
const pool = this.poolManager.getPool(poolId);
if (!pool) {
throw new Error(`Pool ${poolId} not found`);
}
console.log(`[SwapSig] Waiting for settlement confirmations for pool ${poolId.substring(0, 8)}...`);
console.log(`[SwapSig] Waiting for MuSig2 coordination...`);
await new Promise(resolve => setTimeout(resolve, 5000));
console.log(`[SwapSig] MuSig2 coordination complete (placeholder)`);
if (!pool.settlementMapping || pool.settlementMapping.size === 0) {
console.warn(`[SwapSig] No settlement transactions to monitor`);
this.poolManager.transitionPhase(poolId, SwapPhase.COMPLETE);
this.emit(SwapSigEvent.SETTLEMENT_COMPLETE, poolId);
this.emit(SwapSigEvent.POOL_COMPLETE, poolId);
return;
}
const settlementTxIds = [];
for (const settlement of pool.settlementMapping.values()) {
}
if (settlementTxIds.length > 0) {
const requiredConfs = this.swapConfig.requiredConfirmations || 1;
const pollInterval = this.swapConfig.confirmationPollInterval || 3000;
console.log(`[SwapSig] Waiting for ${requiredConfs} confirmations on ${settlementTxIds.length} settlement transactions...`);
const confirmationPromises = settlementTxIds.map(txId => this.txMonitor.waitForConfirmations(txId, requiredConfs, pollInterval, this.swapConfig.settlementTimeout));
const results = await Promise.all(confirmationPromises);
const allConfirmed = results.every(result => result?.isConfirmed);
if (!allConfirmed) {
console.error(`[SwapSig] Settlement confirmation timeout for pool ${poolId.substring(0, 8)}...`);
this.poolManager.abortPool(poolId, 'Settlement confirmation timeout');
this.emit(SwapSigEvent.POOL_ABORTED, poolId, 'Settlement confirmation timeout');
throw new Error('Settlement confirmation timeout');
}
console.log(`[SwapSig] All settlement transactions confirmed!`);
}
else {
console.log(`[SwapSig] No settlement transaction IDs available (placeholder mode)`);
}
for (const settlement of pool.settlementMapping.values()) {
settlement.confirmed = true;
}
this.poolManager.transitionPhase(poolId, SwapPhase.COMPLETE);
this.emit(SwapSigEvent.SETTLEMENT_COMPLETE, poolId);
this.emit(SwapSigEvent.POOL_COMPLETE, poolId);
console.log(`[SwapSig] Pool ${poolId.substring(0, 8)}... complete! 🎉`);
}
_getMyParticipant(pool) {
const myPeerId = this.libp2pNode.peerId.toString();
const participant = pool.participantMap.get(myPeerId);
if (!participant) {
throw new Error('Not a participant in this pool');
}
return participant;
}
_encryptAddress(address, poolId) {
const addressStr = address.toString();
const addressBuf = Buffer.from(addressStr, 'utf8');
if (!this.swapConfig.requireEncryptedDestinations) {
return addressBuf;
}
const poolSecret = Hash.sha256(Buffer.from(poolId, 'hex'));
const encrypted = Buffer.alloc(addressBuf.length);
for (let i = 0; i < addressBuf.length; i++) {
encrypted[i] = addressBuf[i] ^ poolSecret[i % poolSecret.length];
}
return encrypted;
}
_decryptAddress(encryptedAddress, poolId) {
if (!this.swapConfig.requireEncryptedDestinations) {
return Address.fromString(encryptedAddress.toString('utf8'));
}
const poolSecret = Hash.sha256(Buffer.from(poolId, 'hex'));
const decrypted = Buffer.alloc(encryptedAddress.length);
for (let i = 0; i < encryptedAddress.length; i++) {
decrypted[i] = encryptedAddress[i] ^ poolSecret[i % poolSecret.length];
}
return Address.fromString(decrypted.toString('utf8'));
}
_generateMessageId() {
return Hash.sha256(Buffer.concat([
Buffer.from(Date.now().toString()),
Buffer.from(Math.random().toString()),
])).toString('hex');
}
_validatePoolAnnouncement(announcement) {
try {
const announcementData = Buffer.concat([
Buffer.from(announcement.poolId, 'hex'),
Buffer.from(announcement.denomination.toString()),
Buffer.from(announcement.createdAt.toString()),
]);
const announcementHash = Hash.sha256(announcementData);
const signature = Signature.fromBuffer(announcement.creatorSignature, false);
return Schnorr.verify(announcementHash, signature, this.privateKey.publicKey);
}
catch (error) {
console.error('[SwapSig] Error validating pool announcement:', error);
return false;
}
}
async _handlePoolJoin(poolId, participantIndex, peerId) {
console.log(`[SwapSig] Peer ${peerId} joined pool ${poolId.substring(0, 8)}... as participant ${participantIndex}`);
this.emit(SwapSigEvent.POOL_JOINED, poolId, participantIndex);
}
async _handleParticipantRegistered(poolId, participantIndex, peerId, publicKey, inputTxId, inputIndex, ownershipProof, finalOutputCommitment, fromPeerId) {
const pool = this.poolManager.getPool(poolId);
if (!pool) {
console.warn(`[SwapSig] Received participant registration for unknown pool ${poolId.substring(0, 8)}...`);
return;
}
if (pool.participantMap.has(peerId)) {
console.warn(`[SwapSig] Participant ${peerId} already registered in pool ${poolId.substring(0, 8)}...`);
return;
}
console.log(`[SwapSig] Participant ${peerId} registered in pool ${poolId.substring(0, 8)}... from ${fromPeerId}`);
this.emit(SwapSigEvent.PARTICIPANT_JOINED, poolId, {
peerId,
participantIndex,
publicKey,
input: {
txId: inputTxId,
outputIndex: inputIndex,
amount: pool.denomination,
script: pool.participants[participantIndex]?.input.script,
address: pool.participants[participantIndex]?.input.address,
},
ownershipProof,
finalOutputEncrypted: Buffer.alloc(0),
finalOutputCommitment,
setupConfirmed: false,
joinedAt: Date.now(),
});
}
async _handleRegistrationAck(poolId, participantIndex, acknowledgedBy, fromPeerId) {
console.log(`[SwapSig] Registration ACK for participant ${participantIndex} in pool ${poolId.substring(0, 8)}... from ${fromPeerId}`);
}
async _handleSetupTxBroadcast(poolId, participantIndex, txId, fromPeerId) {
const pool = this.poolManager.getPool(poolId);
if (!pool) {
console.warn(`[SwapSig] Received setup tx broadcast for unknown pool ${poolId.substring(0, 8)}...`);
return;
}
console.log(`[SwapSig] Setup tx ${txId.substring(0, 8)}... broadcast for participant ${participantIndex} in pool ${poolId.substring(0, 8)}... from ${fromPeerId}`);
if (pool.participants[participantIndex]) {
pool.participants[participantIndex].setupTxId = txId;
}
this.emit(SwapSigEvent.SETUP_TX_BROADCAST, poolId, participantIndex, txId);
}
async _handleSetupConfirmed(poolId, participantIndex, txId, confirmations, fromPeerId) {
const pool = this.poolManager.getPool(poolId);
if (!pool)
return;
console.log(`[SwapSig] Setup tx ${txId.substring(0, 8)}... confirmed (${confirmations} confs) for participant ${participantIndex} in pool ${poolId.substring(0, 8)}...`);
if (pool.participants[participantIndex]) {
pool.participants[participantIndex].setupConfirmed = true;
}
this.emit(SwapSigEvent.SETUP_CONFIRMED, poolId, participantIndex);
if (this.poolManager.allSetupsConfirmed(poolId)) {
this.poolManager.transitionPhase(poolId, SwapPhase.REVEAL);
this.emit(SwapSigEvent.SETUP_COMPLETE, poolId);
}
}
async _handleSetupComplete(poolId, fromPeerId) {
console.log(`[SwapSig] Setup complete for pool ${poolId.substring(0, 8)}... from ${fromPeerId}`);
this.emit(SwapSigEvent.SETUP_COMPLETE, poolId);
}
async _handleDestinationReveal(poolId, participantIndex, finalAddress, revealProof, fromPeerId) {
const pool = this.poolManager.getPool(poolId);
if (!pool)
return;
console.log(`[SwapSig] Destination revealed for participant ${participantIndex} in pool ${poolId.substring(0, 8)}...`);
const participant = pool.participants[participantIndex];
if (participant) {
participant.finalAddress = finalAddress;
}
this.emit(SwapSigEvent.DESTINATION_REVEALED, poolId, participantIndex, finalAddress);
if (this.poolManager.allDestinationsRevealed(poolId)) {
this.poolManager.transitionPhase(poolId, SwapPhase.SETTLEMENT);
this.emit(SwapSigEvent.REVEAL_COMPLETE, poolId);
}
}
async _handleRevealComplete(poolId, fromPeerId) {
console.log(`[SwapSig] Reveal complete for pool ${poolId.substring(0, 8)}... from ${fromPeerId}`);
this.emit(SwapSigEvent.REVEAL_COMPLETE, poolId);
}
async _handleSettlementTxBroadcast(poolId, outputIndex, txId, fromPeerId) {
const pool = this.poolManager.getPool(poolId);
if (!pool)
return;
console.log(`[SwapSig] Settlement tx ${txId.substring(0, 8)}... broadcast for output ${outputIndex} in pool ${poolId.substring(0, 8)}...`);
if (pool.sharedOutputs[outputIndex]) {
pool.sharedOutputs[outputIndex].settlementTxId = txId;
}
this.emit(SwapSigEvent.SETTLEMENT_TX_BROADCAST, poolId, outputIndex, txId);
}
async _handleSettlementConfirmed(poolId, outputIndex, txId, confirmations, fromPeerId) {
const pool = this.poolManager.getPool(poolId);
if (!pool)
return;
console.log(`[SwapSig] Settlement tx ${txId.substring(0, 8)}... confirmed (${confirmations} confs) for output ${outputIndex} in pool ${poolId.substring(0, 8)}...`);
if (pool.sharedOutputs[outputIndex]) {
pool.sharedOutputs[outputIndex].settlementConfirmed = true;
}
this.emit(SwapSigEvent.SETTLEMENT_CONFIRMED, poolId, outputIndex);
if (this.poolManager.allSettlementsConfirmed(poolId)) {
this.poolManager.transitionPhase(poolId, SwapPhase.COMPLETE);
this.emit(SwapSigEvent.SETTLEMENT_COMPLETE, poolId);
this.emit(SwapSigEvent.POOL_COMPLETE, poolId);
}
}
async _handleSettlementComplete(poolId, fromPeerId) {
console.log(`[SwapSig] Settlement complete for pool ${poolId.substring(0, 8)}... from ${fromPeerId}`);
this.emit(SwapSigEvent.SETTLEMENT_COMPLETE, poolId);
}
async _handlePoolAbort(poolId, reason, fromPeerId) {
console.log(`[SwapSig] Pool ${poolId.substring(0, 8)}... aborted by ${fromPeerId}: ${reason}`);
this.poolManager.abortPool(poolId, reason);
this.emit(SwapSigEvent.POOL_ABORTED, poolId, reason);
}
async _handleParticipantDropped(poolId, peerId, reason, fromPeerId) {
console.log(`[SwapSig] Participant ${peerId} dropped from pool ${poolId.substring(0, 8)}... by ${fromPeerId}: ${reason}`);
this.poolManager.removeParticipant(poolId, peerId);
this.emit(SwapSigEvent.PARTICIPANT_DROPPED, poolId, peerId);
}
_onSwapSigPeerConnected(peerId) {
console.log(`[SwapSig] Peer connected: ${peerId}`);
}
_onSwapSigPeerDisconnected(peerId) {
console.log(`[SwapSig] Peer disconnected: ${peerId}`);
const pools = this.poolManager.getAllPools();
for (const pool of pools) {
if (pool.participantMap.has(peerId)) {
if (pool.phase === SwapPhase.DISCOVERY ||
pool.phase === SwapPhase.REGISTRATION) {
this.poolManager.removeParticipant(pool.poolId, peerId);
this.emit(SwapSigEvent.PARTICIPANT_DROPPED, pool.poolId, peerId);
}
else {
this.poolManager.abortPool(pool.poolId, `Participant ${peerId} disconnected`);
this.emit(SwapSigEvent.POOL_ABORTED, pool.poolId, `Participant ${peerId} disconnected`);
}
}
}
}
async _sendMessageToPeer(peerId, messageType, payload) {
await this.broadcast({
type: messageType,
from: this.libp2pNode.peerId.toString(),
to: peerId,
payload,
timestamp: Date.now(),
messageId: this._generateMessageId(),
protocol: 'swapsig',
});
}
}