Skip to content

Instantly share code, notes, and snippets.

@andrewgordstewart
Created July 20, 2020 22:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andrewgordstewart/d1adecedd9d9d2cfa0ab5b99aa44aa17 to your computer and use it in GitHub Desktop.
Save andrewgordstewart/d1adecedd9d9d2cfa0ab5b99aa44aa17 to your computer and use it in GitHub Desktop.
process.env.NODE_ENV = 'development';
require('../env');
import {
StateVariablesWithHash,
StateVariables,
} from '@statechannels/wallet-core';
import {
Machine,
MachineConfig,
send,
sendParent,
Action,
StateMachine,
State,
assign,
} from 'xstate';
import { Channel } from '../models/channel';
import _ from 'lodash';
type PresentStage = 'PrefundSetup' | 'PostfundSetup' | 'Running' | 'Final';
type Stage = PresentStage | 'Missing';
type StagedState =
| { stage: 'Missing' }
| ({ stage: PresentStage } & StateVariablesWithHash);
export type ChannelState = {
channelId: string;
myIndex: 0 | 1;
peer: ParticipantId;
hub?: ParticipantId;
// turn number == 0 <==> prefund setup
// turn number == 3 <==> postfund setup
// turn number > 3 <=> channel is set up
supported: StagedState;
latest: StagedState;
myLatest: StagedState;
};
export function protocolState(channel: Channel): ProtocolState {
const { channelId, myIndex, participants } = channel;
const peer = participants[1 - myIndex].participantId;
const supported = stage(channel.supported);
const latest = stage(channel.latest);
const myLatest = stage(channel.latestSignedByMe);
return {
app: {
myIndex: myIndex as 0 | 1,
channelId,
supported,
latest,
myLatest,
peer,
},
};
}
const stage = (state?: StateVariablesWithHash): StagedState =>
!state
? { stage: 'Missing' }
: {
...state,
stage: state.isFinal
? 'Final'
: state.turnNum === 0
? 'PrefundSetup'
: state.turnNum === 3
? 'PostfundSetup'
: 'Running',
};
type StateUpdate = {
type: 'StateUpdate';
channelId: Bytes32;
} & Partial<StateVariables>;
type SupportState = {
type: 'SupportState';
channelId: Bytes32;
hash: Bytes32;
};
type SendMessage = { type: 'SendMessage'; message: any; to: string };
type Notify = { type: 'Notify'; message: any };
type TAction = StateUpdate | SendMessage | Notify | SupportState;
type ProtocolState = { app: ChannelState };
const initial = 'init';
// const signLatest: Action<ProtocolState, any> = (ps: ProtocolState) => [
// sendParent({
// type: 'SupportState',
// channelId: ps.app.channelId,
// hash: ps.app.latest.stateHash,
// }),
// ];
const signPrefundState: M = {
initial,
states: {
init: {
entry: () => send('SignLatest'),
on: { SignLatest: { target: 'done', actions: 'signLatest' } },
},
},
};
const signPostfundState: Action<ProtocolState, any> = (ps: ProtocolState) => [
sendParent({ type: 'StateUpdate', channelId: ps.app.channelId, turnNum: 3 }),
];
type M = MachineConfig<ProtocolState, any, any>;
const prefundSupported: M = {
initial,
states: {
init: { entry: ps => ps.app.latest, on: { PrefundSetup: 'done' } },
done: { type: 'final', entry: signPostfundState },
},
};
const done = { type: 'final' } as const;
const error = (reason: string) => ({
target: 'error',
actions: assign({ error: reason }) as any,
});
const noSupportedState: M = {
initial,
states: {
init: {
entry: ps => send(ps.app.latest.stage),
on: {
Missing: error('Channel missing'),
PrefundSetup: 'signLatestPrefund',
PostfundSetup: error('You were too eager'),
Running: error('You were too eager'),
},
},
signLatestPrefund: signPrefundState,
},
};
const protocol: M = {
initial,
states: {
init: {
entry: ps => ps.app.supported.stage,
on: {
Missing: 'noSupportedState',
PrefundSetup: 'prefundSupported',
PostfundSetup: 'done',
},
},
noSupportedState,
prefundSupported,
done,
},
};
console.log(JSON.stringify(protocol));
const doAction = _.noop;
// The machine can be used something like this:
export async function executionLoop(c: Channel) {
const tx = await Channel.startTransaction();
const ps = protocolState(c);
const service = new Interpreter(protocol, ps);
service.run();
service.actions.map(doAction);
await tx.commit();
return service.state;
}
const notXstate = action => action.type && !action.type.startsWith('xstate');
class Interpreter {
private machine: StateMachine<ProtocolState, any, any>;
private _state: State<ProtocolState, any>;
private _actions = [];
constructor(private readonly config: M, ctx: ProtocolState) {
this.machine = Machine(this.config).withContext(ctx);
this._state = this.machine.initialState;
}
public run() {
while (!this._state.changed) {
this._state = this.machine.transition(this._state, 'ACT');
this._actions = this._actions.concat(
this._state.actions.filter(notXstate)
);
}
return;
}
get ctx() {
return this._state.context;
}
get state() {
return this._state;
}
get actions() {
return this._actions;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment