Created
July 20, 2020 22:05
-
-
Save andrewgordstewart/d1adecedd9d9d2cfa0ab5b99aa44aa17 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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