import { Machine , MachineConfig , StateNodeConfig , ActionTypes , DoneInvokeEvent , assign } from 'xstate' ; import { filter , map , first } from 'rxjs/operators' ; import _ from 'lodash' ; import { isVirtuallyFund , StateVariables , Outcome , isSimpleEthAllocation , simpleEthAllocation , checkThat , Zero , BN } from '@statechannels/wallet-core' ; import { Store } from '../store' ; import { CHALLENGE_DURATION , HUB , zeroAddress } from '../config' ; import { MessagingServiceInterface } from '../messaging' ; import { getDataAndInvoke } from '../utils' ; import { SupportState , VirtualFundingAsLeaf , Depositing } from '.' ; const PROTOCOL = 'create-and-fund' ; const { add } = BN ; export type Init = { channelId : string ; funding : 'Direct' | 'Virtual' | 'Ledger' ; } ; const isDirect = ( ctx ) => ctx . funding === 'Direct' ; const isVirtual = ( ctx ) => ctx . funding === 'Virtual' ; const preFundSetup = getDataAndInvoke < Init , Service > ( { src : 'getPreFundSetup' } , { src : 'supportState' } , [ { target : 'direct' , cond : isDirect } , { target : 'virtual' , cond : isVirtual } ] ) ; const direct = { initial : 'depositing' , states : { depositing : getDataAndInvoke < Init , Service > ( { src : 'getDepositingInfo' } , { src : 'depositing' } , 'updateFunding' ) , updateFunding : { invoke : { src : 'setFundingToDirect' , onDone : 'done' } } , done : { type : 'final' } } , onDone : 'postFundSetup' } ; const triggerObjective = ( store ) => async ( ctx ) : Promise < void > => { const { channelConstants , supported : supportedState , myIndex } = await store . getEntry ( ctx . channelId ) ; if ( myIndex !== 0 ) return ; const { participants : targetParticipants } = channelConstants ; const participants = [ ... targetParticipants , HUB ] ; const { allocationItems } = checkThat ( supportedState . outcome , isSimpleEthAllocation ) ; const outcome = simpleEthAllocation ( [ allocationItems [ 0 ] , { destination : HUB . destination , amount : allocationItems . map ( i => i . amount ) . reduce ( add ) } , allocationItems [ 1 ] ] ) ; const stateVars = { turnNum : 0 , outcome , appData : '0x' , isFinal : false } ; const { channelId : jointChannelId } = await store . createChannel ( participants , CHALLENGE_DURATION , stateVars ) ; store . addObjective ( { type : 'VirtuallyFund' , participants , data : { jointChannelId , targetChannelId : ctx . channelId } } ) ; } ; const assignJointChannelId = assign < any > ( { jointChannelId : ( _ , event ) => event . data . jointChannelId } ) ; const reserveFunds = ( store , messagingService ) => async ( context ) => { const channelEntry = await store . getEntry ( context . channelId ) ; const { allocationItems } = checkThat ( channelEntry . supported . outcome , isSimpleEthAllocation ) ; const playerAddress = await store . getAddress ( ) ; const playerDestination = channelEntry . supported . participants . find ( p => p . signingAddress === playerAddress ) ?. destination || '0x00' ; const receive = allocationItems . find ( a => a . destination !== playerDestination ) ?. amount || Zero ; const send = allocationItems . find ( a => a . destination === playerDestination ) ?. amount || Zero ; const budget = await store . reserveFunds ( zeroAddress , context . channelId , { receive , send } ) ; await messagingService . sendBudgetNotification ( budget ) ; } ; type VirtualFundingComplete = Init & { jointChannelId : string ; } ; const virtual = { initial : 'reserveFunds' , entry : [ triggerObjective . name ] , states : { reserveFunds : { invoke : { src : 'reserveFunds' , onDone : 'virtualFunding' } } , virtualFunding : getDataAndInvoke < Init , Service > ( { src : 'getObjective' } , { src : 'virtualFunding' , opts : { entry : 'assignJointChannelId' } } , 'updateFunding' ) , updateFunding : { invoke : { src : 'setFundingToVirtual' , onDone : 'done' } } , done : { type : 'final' } } , onDone : 'postFundSetup' } ; const postFundSetup = getDataAndInvoke < Init , Service > ( { src : 'getPostFundSetup' } , { src : 'supportState' } , 'success' ) ; export const config = { key : PROTOCOL , initial : 'preFundSetup' , on : { [ ActionTypes . ErrorCustom ] : { target : 'failure' } } , states : { preFundSetup , direct , virtual , postFundSetup , success : { type : 'final' } , failure : { } } } ; export const services = ( store , messagingService ) => ( { depositing : Depositing . machine ( store ) , supportState : SupportState . machine ( store ) , virtualFunding : VirtualFundingAsLeaf . machine ( store ) , getDepositingInfo : getDepositingInfo ( store ) , getPreFundSetup : getPreFundSetup ( store ) , getPostFundSetup : getPostFundSetup ( store ) , setFundingToDirect : setFundingToDirect ( store ) , setFundingToVirtual : setFundingToVirtual ( store ) , getObjective : getObjective ( store ) , reserveFunds : reserveFunds ( store , messagingService ) } ) ; type Service = keyof ReturnType < typeof services > ; const options = ( store , messagingService ) => ( { services : services ( store , messagingService ) , actions : { triggerObjective : triggerObjective ( store ) , assignJointChannelId } } ) ; export const machine = ( store , messagingService ) => Machine ( config ) . withConfig ( options ( store , messagingService ) ) ; const getObjective = ( store ) => ( ctx ) : Promise < VirtualFundingAsLeaf . Init > => store . objectiveFeed . pipe ( filter ( isVirtuallyFund ) , map ( ( { data } ) : VirtualFundingAsLeaf . Init => ( { targetChannelId : data . targetChannelId , jointChannelId : data . jointChannelId } ) ) , filter ( ( { targetChannelId } ) => targetChannelId === ctx . channelId ) , first ( ) ) . toPromise ( ) ; const getPreFundSetup = ( store ) => ( ctx ) : Promise < SupportState . Init > => store . channelUpdatedFeed ( ctx . channelId ) . pipe ( map ( e => _ . sortBy ( e . sortedStates , s => s . turnNum ) [ 0 ] ) , filter ( s => s . turnNum === 0 ) , map ( state => ( { state } ) ) , first ( ) ) . toPromise ( ) ; const getPostFundSetup = ( store ) => ( ctx ) : Promise < SupportState . Init > => store . channelUpdatedFeed ( ctx . channelId ) . pipe ( map ( e => _ . sortBy ( e . sortedStates , s => s . turnNum ) [ 0 ] ) , filter ( s => s . turnNum === 0 ) , map ( s => ( { state : { ... s , turnNum : 3 } } ) ) , first ( ) ) . toPromise ( ) ; const getDepositingInfo = ( store ) => async ( { channelId } ) : Promise < Depositing . Init > => { const { supported : supportedState , myIndex } = await store . getEntry ( channelId ) ; const { allocationItems } = checkThat ( supportedState . outcome , isSimpleEthAllocation ) ; const fundedAt = allocationItems . map ( a => a . amount ) . reduce ( add ) ; let depositAt = Zero ; for ( let i = 0 ; i < allocationItems . length ; i ++ ) { const { amount } = allocationItems [ i ] ; if ( i !== myIndex ) depositAt = add ( depositAt , amount ) ; else { const totalAfterDeposit = add ( depositAt , amount ) ; return { channelId , depositAt , totalAfterDeposit , fundedAt } ; } } throw Error ( `Could not find an allocation for participant id ${ myIndex }` ) ; } ; const setFundingToDirect = ( store ) => async ( ctx ) => await store . setFunding ( ctx . channelId , { type : 'Direct' } ) ; const setFundingToVirtual = ( store ) => async ( ctx ) => { await store . setFunding ( ctx . channelId , { type : 'Virtual' , jointChannelId : ctx . jointChannelId } ) ; } ;