import { assign , createMachine , DoneInvokeEvent , State , StateMachine , StateSchema , Guard , spawn , actions } from 'xstate' ; import { map } from 'rxjs/operators' ; import { ChannelChainInfo } from '../chain' ; import { Store } from '../store' ; const { log } = actions ; export interface Initial { channelId : string ; } interface Transaction { transactionId : string ; } type Typestate = { value : 'init' ; context : Initial ; } | { value : 'waitForResponseOrTimeout' ; context : Initial & Transaction ; } | { value : 'submitTransaction' ; context : Initial ; } | { value : 'retry' ; context : Initial & Transaction ; } | { value : 'waitMining' ; context : Initial & Transaction ; } | { value : 'done' ; context : Initial ; } | { value : 'failure' ; context : Initial ; } ; type Context = Typestate [ 'context' ] ; interface Schema extends StateSchema < Context > { states : { init : { } ; submitTransaction : { } ; retry : { } ; waitMining : { } ; waitForResponseOrTimeout : { } ; done : { } ; failure : { } ; } ; } export type WorkflowState = State < Context , Event , Schema , Typestate > ; export type StateValue = keyof Schema [ 'states' ] ; interface ChainEvent { type : 'CHAIN_EVENT' ; turnNumRecord : number ; finalizesAt : number ; finalized : boolean ; } const noChallengeOnchain = { type : 'xstate.guard' , name : 'noChallengeOnchain' , predicate : ( context , { finalizesAt } ) => finalizesAt === 0 } ; const challengeOnchainAsExpected = { type : 'xstate.guard' , name : 'challengeOnchainAsExpected' , predicate : ( context , { finalizesAt , finalized } ) => finalizesAt > 0 && ! finalized } ; const challengeFinalized = { type : 'xstate.guard' , name : 'challengeFinalized' , predicate : ( context , { finalized } ) => finalized } ; const submitChallengeTransaction = ( store ) => async ( { channelId } ) => { const { support , myAddress } = await store . getEntry ( channelId ) ; const privateKey = await store . getPrivateKey ( myAddress ) ; return await store . chain . challenge ( support , privateKey ) ; } ; const observeOnChainChannelStorage = ( store , channelId ) => store . chain . chainUpdatedFeed ( channelId ) . pipe ( map < ChannelChainInfo , ChainEvent > ( ( { finalized , channelStorage } ) => ( { type : 'CHAIN_EVENT' , finalized , ... channelStorage } ) ) ) ; const setTransactionId = assign < Context , DoneInvokeEvent < string > > ( { transactionId : ( context , { data } ) => data } ) ; export const machine = ( store , context ) : StateMachine < Context , Schema , Event , Typestate > => createMachine < Context , Event , Typestate > ( { context , strict : true , id : 'challenge-channel' , initial : 'init' , on : { CHANNEL_UPDATED : { actions : log ( 'CHANNEL_UPDATED sent to challenge channel machine' ) } , CHAIN_EVENT : [ { target : 'waitForResponseOrTimeout' , cond : challengeOnchainAsExpected } , { target : 'done' , cond : challengeFinalized } ] } , states : { init : { entry : assign < any > ( { chainWatcher : ( { channelId } ) => spawn ( observeOnChainChannelStorage ( store , channelId ) ) } ) , on : { CHAIN_EVENT : { target : 'submitTransaction' , cond : noChallengeOnchain } } } , waitForResponseOrTimeout : { } , submitTransaction : { invoke : { id : 'submitTransaction' , src : submitChallengeTransaction ( store ) , onDone : { target : 'waitMining' , actions : setTransactionId } } } , waitMining : { } , retry : { on : { USER_APPROVES_RETRY : { target : 'submitTransaction' } , USER_REJECTS_RETRY : { target : 'failure' } } } , done : { id : 'done' , type : 'final' } , failure : { id : 'failure' , type : 'final' } } } ) ;