import { Machine , MachineConfig } from 'xstate' ; import _ from 'lodash' ; import { ethers } from 'ethers' ; import { isSimpleEthAllocation , simpleEthAllocation , checkThat , Outcome , SimpleAllocation , AllocationItem , Destination , BN , Zero , makeAddress } from '@statechannels/wallet-core' ; import { Store } from '../store' ; import { getDataAndInvoke2 , MachineFactory } from '../utils/workflow-utils' ; import * as Depositing from './depositing' ; import * as SupportState from './support-state' ; const { add , sub : subtract } = BN ; const WORKFLOW = 'direct-funding' ; export interface Init { channelId : string ; minimalAllocation : AllocationItem [ ] ; } const checkCurrentLevel = { invoke : { src : 'checkCurrentLevel' , onDone : 'updatePrefundOutcome' } } ; export const config = { key : WORKFLOW , initial : 'checkCurrentLevel' , states : { checkCurrentLevel , updatePrefundOutcome : getDataAndInvoke2 ( 'getPrefundOutcome' , 'supportState' , 'funding' ) , funding : getDataAndInvoke2 ( 'getDepositingInfo' , 'fundingService' , 'updatePostfundOutcome' ) , updatePostfundOutcome : getDataAndInvoke2 ( 'getPostfundOutcome' , 'supportState' , 'success' ) , success : { type : 'final' } , failure : { type : 'final' } } } ; type Services = { checkCurrentLevel ( ctx ) : Promise < void > ; getPrefundOutcome ( ctx ) : Promise < SupportState . Init > ; getPostfundOutcome ( ctx ) : Promise < SupportState . Init > ; getDepositingInfo ( ctx ) : Promise < Depositing . Init > ; fundingService : any ; supportState : any ; } ; type Options = { services : Services ; } ; export const machine = ( store , context ) => { async function checkCurrentLevel ( ctx ) { const { supported : supportedState } = await store . getEntry ( ctx . channelId ) ; const outcome = checkThat ( supportedState . outcome , isSimpleEthAllocation ) ; const allocated = outcome . allocationItems . map ( a => a . amount ) . reduce ( BN . add , Zero ) ; const chainInfo = await store . chain . getChainInfo ( ctx . channelId ) ; if ( BN . gt ( allocated , chainInfo . amount ) ) throw new Error ( 'DirectFunding: Channel outcome is already underfunded; aborting' ) ; } function minimalOutcome ( currentOutcome , minimalEthAllocation ) { const allocationItems = currentOutcome . allocationItems . concat ( minimalEthAllocation . map ( ( { destination , amount } ) => { const currentlyAllocated = currentOutcome . allocationItems . filter ( i => i . destination === destination ) . map ( i => i . amount ) . reduce ( add ) ; const amountLeft = BN . gt ( amount , currentlyAllocated ) ? subtract ( amount , currentlyAllocated ) : Zero ; return { destination , amount : amountLeft } ; } ) ) ; return simpleEthAllocation ( allocationItems ) ; } function mergeDestinations ( outcome ) { const destinations = _ . uniq ( outcome . allocationItems . map ( i => i . destination ) ) ; const allocationItems = destinations . map ( destination => ( { destination , amount : outcome . allocationItems . filter ( i => i . destination === destination ) . map ( i => i . amount ) . reduce ( add ) } ) ) ; return simpleEthAllocation ( allocationItems ) ; } async function getDepositingInfo ( { minimalAllocation , channelId } ) { const { supported : supportedState , myIndex } = await store . getEntry ( channelId ) ; const supportedOutcome = supportedState . outcome ; if ( ! isSimpleEthAllocation ( supportedOutcome ) ) { throw new Error ( 'Unsupported outcome' ) ; } let totalBeforeDeposit = Zero ; for ( let i = 0 ; i < minimalAllocation . length ; i ++ ) { const allocation = minimalAllocation [ i ] ; if ( myIndex === i ) { const fundedAt = supportedOutcome . allocationItems . map ( a => a . amount ) . reduce ( add ) ; return { channelId , depositAt : totalBeforeDeposit , totalAfterDeposit : BN . add ( totalBeforeDeposit , allocation . amount ) , fundedAt } ; } else { totalBeforeDeposit = BN . add ( allocation . amount , totalBeforeDeposit ) ; } } throw Error ( `Could not find an allocation for participant id ${ myIndex }` ) ; } async function getPrefundOutcome ( { channelId , minimalAllocation } ) { const entry = await store . getEntry ( channelId ) ; const { channelConstants } = entry ; if ( minimalAllocation . length !== channelConstants . participants . length ) { throw new Error ( 'Must be exactly one allocation item per participant' ) ; } if ( entry . isSupported ) { const outcome = minimalOutcome ( entry . latest . outcome as SimpleAllocation , minimalAllocation ) ; return { state : { ... entry . latest , ... entry . channelConstants , outcome , turnNum : entry . latest . turnNum + 1 } } ; } else { return { state : { ... entry . channelConstants , challengeDuration : 1 , isFinal : false , turnNum : 0 , outcome : minimalOutcome ( simpleEthAllocation ( [ ] ) , minimalAllocation ) , appData : ethers . constants . HashZero , appDefinition : makeAddress ( ethers . constants . AddressZero ) } } ; } } async function getPostfundOutcome ( { channelId } ) { const { supported } = await store . getEntry ( channelId ) ; return { state : { ... supported , turnNum : supported . turnNum + 1 , outcome : mergeDestinations ( supported . outcome as SimpleAllocation ) } } ; } const services = { checkCurrentLevel , getPrefundOutcome , supportState : SupportState . machine ( store ) , fundingService : Depositing . machine ( store ) , getPostfundOutcome , getDepositingInfo } ; const options = { services } ; return Machine ( config ) . withConfig ( options , context ) ; } ;