import { ServiceConfig , StateMachine , Machine , ActionFunction , DoneInvokeEvent , assign , MachineConfig , AssignAction , Interpreter } from 'xstate' ; import { outcomesEqual , Participant , Objective , CloseLedger , DomainBudget , BN } from '@statechannels/wallet-core' ; import { map , filter } from 'rxjs/operators' ; import { ChannelChainInfo } from '../chain' ; import { Store } from '../store' ; import { MessagingServiceInterface } from '../messaging' ; import { getDataAndInvoke } from '../utils' ; import { CommonWorkflowActions , commonWorkflowActions , CommonActions } from '../utils/workflow-utils' ; import { SupportState } from '.' ; interface Initial { requestId : number ; opponent : Participant ; player : Participant ; domain : string ; } interface BudgetExists extends Initial { budget : DomainBudget ; } interface LedgerExists extends BudgetExists { ledgerId : string ; } interface Transaction { transactionId : string ; } interface FundsWithdrawn { type : 'FUNDS_WITHDRAWN' ; } type WorkflowTypeState = { value : 'fetchBudget' ; context : Initial ; } | { value : 'waitForUserApproval' ; context : BudgetExists ; } | { value : 'createObjective' ; context : LedgerExists ; } | { value : { closeLedger : 'constructFinalState' ; } ; context : LedgerExists ; } | { value : { closeLedger : 'supportState' ; } ; context : LedgerExists ; } | { value : { closeLedger : 'done' ; } ; context : LedgerExists ; } | { value : { withdraw : 'submitTransaction' ; } ; context : LedgerExists ; } | { value : { withdraw : 'waitMining' ; } ; context : LedgerExists & Transaction ; } | { value : { withdraw : 'done' ; } ; context : LedgerExists ; } | { value : 'done' ; context : LedgerExists ; } | { value : 'clearBudget' ; context : LedgerExists ; } | { value : 'budgetFailure' ; context : Initial ; } | { value : 'userDeclinedFailure' ; context : Initial ; } ; export type WorkflowContext = WorkflowTypeState [ 'context' ] ; export type WorkflowEvent = UserApproves | UserRejects | DoneInvokeEvent < CloseLedger > | DoneInvokeEvent < LedgerExists > | DoneInvokeEvent < DomainBudget > | DoneInvokeEvent < string > | FundsWithdrawn ; interface UserApproves { type : 'USER_APPROVES_CLOSE' ; } interface UserRejects { type : 'USER_REJECTS_CLOSE' ; } enum Actions { sendResponse = 'sendResponse' , assignLedgerId = 'assignLedgerId' , setTransactionId = 'setTransactionId' , assignBudget = 'assignBudget' } enum Services { constructFinalState = 'constructFinalState' , supportState = 'supportState' , submitWithdrawTransaction = 'submitWithdrawTransaction' , createObjective = 'createObjective' , observeFundsWithdrawal = 'observeFundsWithdrawal' , fetchBudget = 'fetchBudget' , clearBudget = 'clearBudget' } export type WorkflowActions = CommonWorkflowActions & Record < Actions , ActionFunction < WorkflowContext , WorkflowEvent > | AssignAction < WorkflowContext , WorkflowEvent > > ; export type WorkflowServices = Record < Services , ServiceConfig < WorkflowContext > > ; export const config = { id : 'close-and-withdraw' , initial : 'fetchBudget' , states : { fetchBudget : { invoke : { src : Services . fetchBudget , onDone : { target : 'waitForUserApproval' , actions : [ Actions . assignBudget ] } } } , waitForUserApproval : { entry : [ CommonActions . displayUI ] , on : { USER_APPROVES_CLOSE : { target : 'createObjective' } , USER_REJECTS_CLOSE : { target : 'userDeclinedFailure' } } } , createObjective : { invoke : { src : Services . createObjective , onDone : { target : 'closeLedger' , actions : [ Actions . assignLedgerId ] } } } , closeLedger : getDataAndInvoke < LedgerExists > ( { src : Services . constructFinalState } , { src : Services . supportState } , 'withdraw' ) as any , withdraw : { initial : 'submitTransaction' , on : { FUNDS_WITHDRAWN : 'clearBudget' } , invoke : { id : 'observeChain' , src : Services . observeFundsWithdrawal } , states : { submitTransaction : { invoke : { id : 'submitTransaction' , src : Services . submitWithdrawTransaction , onDone : { target : 'waitMining' , actions : [ Actions . setTransactionId ] } } } , waitMining : { } } } , clearBudget : { invoke : { src : Services . clearBudget , onDone : 'done' , onError : 'budgetFailure' } } , done : { type : 'final' , entry : [ Actions . sendResponse , CommonActions . hideUI ] } , userDeclinedFailure : { type : 'final' , entry : [ CommonActions . hideUI , CommonActions . sendUserDeclinedErrorResponse ] } , budgetFailure : { type : 'final' , entry : [ CommonActions . hideUI ] } } } ; const constructFinalState = ( store ) => async ( { opponent : hub } ) => { const { latestSignedByMe : latestSupportedByMe , latest } = await store . getLedger ( hub . participantId ) ; if ( latest . isFinal && outcomesEqual ( latestSupportedByMe . outcome , latest . outcome ) ) { return { state : latest } ; } if ( latestSupportedByMe . isFinal ) { return { state : latestSupportedByMe } ; } return { state : { ... latestSupportedByMe , turnNum : latestSupportedByMe . turnNum + 1 , isFinal : true } } ; } ; const submitWithdrawTransaction = ( store ) => async ( context ) => { const ledgerEntry = await store . getLedger ( context . opponent . participantId ) ; if ( ! ledgerEntry . hasConclusionProof ) { throw new Error ( `Channel ${ ledgerEntry . channelId } is not finalized` ) ; } return store . chain . finalizeAndWithdraw ( ledgerEntry . support ) ; } ; const createObjective = ( store ) => async ( context ) => { const ledgerEntry = await store . getLedger ( context . opponent . participantId ) ; const objective = { type : 'CloseLedger' , participants : [ context . player , context . opponent ] , data : { ledgerId : ledgerEntry . channelId } } ; await store . addObjective ( objective ) ; return objective ; } ; const observeFundsWithdrawal = ( store ) => ( { ledgerId } ) => store . chain . chainUpdatedFeed ( ledgerId ) . pipe ( filter ( c => BN . eq ( c . amount , 0 ) ) , map < ChannelChainInfo , FundsWithdrawn > ( ( ) => ( { type : 'FUNDS_WITHDRAWN' } ) ) ) ; const clearBudget = ( store ) : ServiceConfig < Initial > => async ( context ) => { await store . clearBudget ( context . domain ) ; } ; const fetchBudget = ( store ) : ServiceConfig < Initial > => async ( context ) => store . getBudget ( context . domain ) ; const assignBudget = ( ) : AssignAction < Initial , DoneInvokeEvent < DomainBudget > > => assign ( ( context , event ) => ( { ... context , budget : event . data } ) ) ; const options = ( store , messagingService ) : { services : WorkflowServices ; actions : WorkflowActions ; } => ( { services : { constructFinalState : constructFinalState ( store ) , supportState : SupportState . machine ( store ) , submitWithdrawTransaction : submitWithdrawTransaction ( store ) , createObjective : createObjective ( store ) , observeFundsWithdrawal : observeFundsWithdrawal ( store ) , clearBudget : clearBudget ( store ) , fetchBudget : fetchBudget ( store ) } , actions : { ... commonWorkflowActions ( messagingService ) , assignBudget : assignBudget ( ) , setTransactionId : assign ( { transactionId : ( context , event ) => event . data } ) , sendResponse : async ( context ) => await messagingService . sendResponse ( context . requestId , { success : true } ) , assignLedgerId : assign ( ( context , event ) => ( { ... context , ledgerId : event . data . data . ledgerId } ) ) } } ) ; export const workflow = ( store , messagingService , context ) : StateMachine < WorkflowContext , any , WorkflowEvent , WorkflowTypeState > => Machine ( config ) . withConfig ( options ( store , messagingService ) ) . withContext ( context ) ; export type CloseLedgerAndWithdrawService = Interpreter < WorkflowContext , any , WorkflowEvent , WorkflowTypeState > ;