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