import { Machine , MachineOptions , AnyEventObject , DoneInvokeEvent , ServiceConfig , assign , StateNodeConfig , ActionTypes } from 'xstate' ; import { filter , map , take , flatMap , tap , first } from 'rxjs/operators' ; import { checkThat , simpleEthGuarantee , isSimpleEthAllocation , simpleEthAllocation , makeDestination , FundGuarantor , AllocationItem , BN } from '@statechannels/wallet-core' ; import { Store , supportedStateFeed } from '../store' ; import { getDataAndInvoke } from '../utils/helpers' ; import { CHALLENGE_DURATION } from '../config' ; import { escalate } from '../actions' ; import { assignError } from '../utils/workflow-utils' ; import { SupportState , LedgerFunding } from '.' ; export const enum OutcomeIdx { A = 0 , Hub = 1 , B = 2 } export const enum ParticipantIdx { A = 0 , B = 1 , Hub = 2 } export type Init = { targetChannelId : string ; jointChannelId : string ; } ; type Deductions = { deductions : AllocationItem [ ] ; } ; type WithDeductions = Init & Deductions ; type WithObjectiveData = WithDeductions & { guarantorChannelId : string ; ledgerId : string ; } ; const getFundGuarantorObjective = ( store ) => async ( ctx ) : Promise < FundGuarantor > => { const { jointChannelId , targetChannelId } = ctx ; const entry = await store . getEntry ( jointChannelId ) ; const { participants : jointParticipants } = entry . channelConstants ; const participants = [ jointParticipants [ entry . myIndex ] , jointParticipants [ ParticipantIdx . Hub ] ] ; const { channelId : ledgerId } = await store . getLedger ( jointParticipants [ ParticipantIdx . Hub ] . participantId ) ; const { channelId : guarantorId } = await store . createChannel ( participants , CHALLENGE_DURATION , { turnNum : 0 , appData : '0x' , isFinal : false , outcome : simpleEthGuarantee ( jointChannelId , targetChannelId , ... participants . map ( p => p . destination ) ) } ) ; return { type : 'FundGuarantor' , participants , data : { jointChannelId , ledgerId , guarantorId } } ; } ; type TEvent = AnyEventObject ; const enum Actions { triggerGuarantorObjective = 'triggerGuarantorObjective' , assignDeductions = 'assignDeductions' , assignGuarantorId = 'assignGuarantorId' } export const enum States { determineDeductions = 'determineDeductions' , setupJointChannel = 'setupJointChannel' , fundJointChannel = 'fundJointChannel' , fundTargetChannel = 'fundTargetChannel' , failure = '#workflow.failure' , success = 'success' } const enum Services { getDeductions = 'getDeductions' , waitForSupportedGuarantorState = 'waitForSupportedGuarantorState' , waitForFirstJointState = 'waitForFirstJointState' , jointChannelUpdate = 'jointChannelUpdate' , supportState = 'supportState' , ledgerFunding = 'ledgerFunding' , fundGuarantor = 'fundGuarantor' , updateJointChannelFunding = 'updateJointChannelFunding' } const waitForSupportedGuarantorState = ( store ) => async ( ctx ) => store . channelUpdatedFeed ( ctx . guarantorChannelId ) . pipe ( filter ( u => u . isSupported ) , first ( ) ) . toPromise ( ) ; export const config = { key : 'virtual-funding-as-leaf' , id : 'workflow' , initial : States . setupJointChannel , on : { [ ActionTypes . ErrorCustom ] : '#workflow.failure' } , states : { [ States . setupJointChannel ] : getDataAndInvoke < Init , Services > ( { src : Services . waitForFirstJointState , opts : { onError : '#workflow.failure' } } , { src : Services . supportState } , States . determineDeductions ) , [ States . determineDeductions ] : { invoke : { src : Services . getDeductions , data : ctx => ctx , onDone : States . fundJointChannel } , exit : Actions . assignDeductions } , [ States . fundJointChannel ] : { initial : 'getObjective' , states : { getObjective : { invoke : { src : Services . fundGuarantor , onDone : 'preparingGuarantorChannel' , onError : '#workflow.failure' } , exit : [ Actions . triggerGuarantorObjective , Actions . assignGuarantorId ] } , preparingGuarantorChannel : { invoke : { src : Services . waitForSupportedGuarantorState , onDone : 'runObjective' } } , runObjective : { invoke : { src : Services . ledgerFunding , data : ( ctx ) : LedgerFunding . Init => ( { targetChannelId : ctx . guarantorChannelId , ledgerChannelId : ctx . ledgerId , deductions : ctx . deductions } ) , onDone : 'updateFunding' } } , updateFunding : { invoke : { src : Services . updateJointChannelFunding , onDone : 'done' } } , done : { type : 'final' } } , onDone : States . fundTargetChannel } , [ States . fundTargetChannel ] : getDataAndInvoke ( { src : Services . jointChannelUpdate } , { src : Services . supportState } , States . success ) , success : { type : 'final' } , failure : { entry : [ assignError , escalate ( ( { error } ) => ( { type : 'FAILURE' , error } ) ) ] } } } ; export const waitForFirstJointState = ( store ) => ( { jointChannelId } ) : Promise < SupportState . Init > => store . channelUpdatedFeed ( jointChannelId ) . pipe ( flatMap ( e => e . sortedStates ) , filter ( ( { turnNum } ) => turnNum === 0 ) , tap ( ( { outcome , participants } ) => { const { allocationItems } = checkThat ( outcome , isSimpleEthAllocation ) ; const destinations = allocationItems . map ( i => i . destination ) ; const amounts = allocationItems . map ( i => i . amount ) ; if ( ! ( destinations [ OutcomeIdx . A ] === participants [ ParticipantIdx . A ] . destination && destinations [ OutcomeIdx . B ] === participants [ ParticipantIdx . B ] . destination && destinations [ OutcomeIdx . Hub ] === participants [ ParticipantIdx . Hub ] . destination ) ) { throw new Error ( 'Incorrect participants' ) ; } else if ( ! ( BN . add ( amounts [ OutcomeIdx . A ] , amounts [ OutcomeIdx . B ] ) === amounts [ OutcomeIdx . Hub ] ) ) { throw new Error ( 'Incorrect allocation' ) ; } else return ; } ) , map ( s => ( { state : s } ) ) , take ( 1 ) ) . toPromise ( ) ; export const jointChannelUpdate = ( store ) => ( { jointChannelId , targetChannelId } ) : Promise < SupportState . Init > => supportedStateFeed ( store , jointChannelId ) . pipe ( filter ( ( { state } ) => state . turnNum === 0 ) , map ( ( { state } ) => { const oldOutcome = checkThat ( state . outcome , isSimpleEthAllocation ) ; const amount = oldOutcome . allocationItems [ OutcomeIdx . Hub ] . amount ; const outcome = simpleEthAllocation ( [ { destination : makeDestination ( targetChannelId ) , amount } , { destination : state . participants [ ParticipantIdx . Hub ] . destination , amount } ] ) ; return { state : { ... state , turnNum : 1 , outcome } } ; } ) , take ( 1 ) ) . toPromise ( ) ; const getDeductions = ( store ) => async ( ctx ) : Promise < Deductions > => { const { latestSignedByMe : latestSupportedByMe , myIndex } = await store . getEntry ( ctx . jointChannelId ) ; const { allocationItems } = checkThat ( latestSupportedByMe . outcome , isSimpleEthAllocation ) ; const outcomeIdx = myIndex === ParticipantIdx . A ? OutcomeIdx . A : OutcomeIdx . B ; return { deductions : [ { destination : allocationItems [ OutcomeIdx . Hub ] . destination , amount : allocationItems [ 2 - outcomeIdx ] . amount } , allocationItems [ outcomeIdx ] ] } ; } ; const updateJointChannelFunding = ( store ) => async ( ctx ) => { const { jointChannelId , guarantorChannelId } = ctx ; await store . setFunding ( jointChannelId , { type : 'Guarantee' , guarantorChannelId } ) ; } ; export const options = ( store ) : Pick < MachineOptions < Init , TEvent > , 'actions' | 'services' > => { const actions = { [ Actions . triggerGuarantorObjective ] : ( _ , { data } ) => store . addObjective ( data ) , [ Actions . assignDeductions ] : assign ( ( ctx , { data } ) : WithDeductions => ( { ... ctx , ... data } ) ) , [ Actions . assignGuarantorId ] : assign ( { guarantorChannelId : ( _ , { data } ) => data . data . guarantorId , ledgerId : ( _ , { data } ) => data . data . ledgerId } ) } ; const services = { waitForSupportedGuarantorState : waitForSupportedGuarantorState ( store ) , getDeductions : getDeductions ( store ) , supportState : SupportState . machine ( store ) , ledgerFunding : LedgerFunding . machine ( store ) , waitForFirstJointState : waitForFirstJointState ( store ) , jointChannelUpdate : jointChannelUpdate ( store ) , fundGuarantor : getFundGuarantorObjective ( store ) , updateJointChannelFunding : updateJointChannelFunding ( store ) } ; return { actions , services } ; } ; export const machine = ( store ) => Machine ( config , options ( store ) ) ;