Skip to content

Instantly share code, notes, and snippets.

@andrewgordstewart
Last active July 20, 2020 22:14
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/c7daea8bfc46e8b7bbf655cafed6f45d to your computer and use it in GitHub Desktop.
Save andrewgordstewart/c7daea8bfc46e8b7bbf655cafed6f45d to your computer and use it in GitHub Desktop.
import { State, StateVariables } from '@statechannels/wallet-core';
import { Channel } from '../models/channel';
type PresentStage = 'PrefundSetup' | 'PostfundSetup' | 'Running' | 'Final';
type Stage = PresentStage | 'Missing';
export type ChannelState = Channel & { peer: string };
// stage is a property with a finite number of values, so it is easy to partition
const stage = (state?: State): Stage =>
!state
? 'Missing'
: state.isFinal
? 'Final'
: state.turnNum === 0
? 'PrefundSetup'
: state.turnNum === 3
? 'PostfundSetup'
: 'Running';
type Partitioner<T> = (ps: ProtocolState) => T;
type Left<E> = { readonly _tag: 'Left'; readonly left: E };
const left = <T>(left: T): Left<T> => ({ _tag: 'Left', left });
type Right<A> = { readonly _tag: 'Right'; readonly right: A };
const right = <T>(right: T): Right<T> => ({ _tag: 'Right', right });
type Either<E, A> = Left<E> | Right<A>;
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 ProtocolAction = StateUpdate | SendMessage | Notify | SupportState;
type ExecutionResult = ProtocolAction[];
type Executor = (ps: ProtocolState) => Either<Error, ExecutionResult>;
type ProtocolState = { app: ChannelState };
const partialMatch = <T extends string>(
partitioner: Partitioner<T>,
executors: Partial<Record<T, Executor>>
) => (ps: ProtocolState) => executors[partitioner(ps)](ps);
const match = <T extends string>(
partitioner: Partitioner<T>,
executors: Record<T, Executor>
) => (ps: ProtocolState) => executors[partitioner(ps)](ps);
const error = (reason: string) => (_ps: ProtocolState) => left(Error(reason));
const signLatestPrefund: Executor = (ps: ProtocolState) => {
if (stage(ps.app.latest) !== 'PrefundSetup') {
return left(Error('latest state is wrong'));
}
return right([
{
type: 'SupportState',
channelId: ps.app.channelId,
hash: 'ps.app.latest.stateHash',
},
]);
};
const prefundAlreadySigned = partialMatch((ps) => stage(ps.app.latest), {
Missing: (ps: ProtocolState) => {
return right([
{
type: 'SendMessage',
message: 'Please sign state 0!',
to: ps.app.peer,
},
]);
},
});
const signPostfundState: Executor = (ps: ProtocolState) =>
right([{ type: 'StateUpdate', channelId: ps.app.channelId, turnNum: 3 }]);
const prefundSupported = partialMatch((ps) => stage(ps.app.latestSignedByMe), {
PrefundSetup: signPostfundState,
NotAMatch: error('This is a type error'),
});
const noSupportedState = match((ps) => stage(ps.app.latestSignedByMe), {
Missing: signLatestPrefund,
PrefundSetup: prefundAlreadySigned,
PostfundSetup: error('You were too eager'),
Running: error('You were too eager'),
// Type error: the following line is needed to not get a type error, since
// the `match` helper requires that you specify executors for each case that the "matcher" can spit out
// Final: error('You were too eager'),
});
export const protocol = partialMatch((ps) => stage(ps.app.supported), {
Missing: noSupportedState,
PrefundSetup: prefundSupported,
PostfundSetup: (ps: ProtocolState) =>
right([{ type: 'Notify', message: `Channel ${ps.app.channelId}is open!` }]),
});
// Note that the `protocol` pattern matching syntax is logically equivalent to the following.
// Something about the `match` syntax above seems well suited to our problem.
// The `match` syntax also makes it possible to generate flow diagrams from the protocols:
// the second argument is defining arrows from where you "are" in your decision making process to where you will "go",
// if the world is a certain way.
export const protocol2: Executor = function (ps: ProtocolState) {
switch (stage(ps.app.supported)) {
case 'Missing':
return noSupportedState(ps);
case 'PrefundSetup':
return prefundSupported(ps);
case 'PostfundSetup':
return right([
{ type: 'Notify', message: `Channel ${ps.app.channelId}is open!` },
]);
case 'Final':
case 'Running':
return left(Error('Whoops'));
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment