UNPKG

lotus-sdk

Version:

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

101 lines (100 loc) 3.84 kB
import { EventEmitter } from 'events'; import { MuSigSessionPhase, } from '../../bitcore/musig2/session.js'; export class SessionStateMachine extends EventEmitter { session; validTransitions = { [MuSigSessionPhase.INIT]: [ MuSigSessionPhase.NONCE_EXCHANGE, MuSigSessionPhase.ABORTED, ], [MuSigSessionPhase.NONCE_EXCHANGE]: [ MuSigSessionPhase.PARTIAL_SIG_EXCHANGE, MuSigSessionPhase.ABORTED, ], [MuSigSessionPhase.PARTIAL_SIG_EXCHANGE]: [ MuSigSessionPhase.COMPLETE, MuSigSessionPhase.ABORTED, ], [MuSigSessionPhase.COMPLETE]: [], [MuSigSessionPhase.ABORTED]: [], }; constructor(session) { super(); this.session = session; if (!this.isValidState(session.phase)) { throw new Error(`Invalid initial phase for session ${session.sessionId}: ${session.phase}`); } } get state() { return this.session.phase; } get sessionId() { return this.session.sessionId; } isValidState(state) { return state in this.validTransitions; } canTransitionTo(toState) { const currentState = this.session.phase; return this.validTransitions[currentState]?.includes(toState) ?? false; } isTerminal() { return this.validTransitions[this.session.phase].length === 0; } transition(toState, reason) { const fromState = this.session.phase; if (!this.canTransitionTo(toState)) { const allowedStates = this.validTransitions[fromState].join(', '); throw new Error(`Invalid state transition for session ${this.session.sessionId}: ` + `${fromState} -> ${toState}. ` + `Allowed transitions from ${fromState}: [${allowedStates}]. ` + `Reason: ${reason || 'no reason provided'}`); } this.session.phase = toState; this.session.updatedAt = Date.now(); if (toState === MuSigSessionPhase.ABORTED && reason) { this.session.abortReason = reason; } const event = { sessionId: this.session.sessionId, fromState, toState, reason: reason || 'state transition', timestamp: this.session.updatedAt, }; this.emit('stateChanged', event); } getValidNextStates() { return [...this.validTransitions[this.session.phase]]; } getStateDescription() { const descriptions = { [MuSigSessionPhase.INIT]: 'Session created, ready to start Round 1', [MuSigSessionPhase.NONCE_EXCHANGE]: 'Round 1 in progress: collecting nonces from all signers', [MuSigSessionPhase.PARTIAL_SIG_EXCHANGE]: 'Round 2 in progress: collecting partial signatures from all signers', [MuSigSessionPhase.COMPLETE]: 'Session complete: signature aggregated successfully', [MuSigSessionPhase.ABORTED]: `Session aborted${this.session.abortReason ? `: ${this.session.abortReason}` : ''}`, }; return descriptions[this.session.phase] || 'Unknown state'; } abort(reason) { if (this.isTerminal()) { return false; } this.transition(MuSigSessionPhase.ABORTED, reason); return true; } getDiagnostics() { const now = Date.now(); return { sessionId: this.session.sessionId, currentState: this.session.phase, stateDescription: this.getStateDescription(), isTerminal: this.isTerminal(), validNextStates: this.getValidNextStates(), createdAt: this.session.createdAt, updatedAt: this.session.updatedAt, age: now - this.session.createdAt, }; } }