import { interpret , State } from 'xstate' ; import { Guid } from 'guid-typescript' ; import { StateChannelsNotification , StateChannelsResponse , StateChannelsErrorResponse } from '@statechannels/client-api-schema' ; import { filter , take } from 'rxjs/operators' ; import { Payload , isOpenChannel , OpenChannel , Address , DirectFunder , makeAddress , RichObjectiveEvent , RichObjective } from '@statechannels/wallet-core' ; import _ from 'lodash' ; import { TriggerObjectiveEvent , UpdateUI , Workflow } from './channel-wallet-types' ; import { serializeChannelEntry } from './utils/wallet-core-v0.8.0' ; import { AppRequestEvent } from './event-types' ; import { Store } from './store' ; import { ApproveBudgetAndFund , CloseLedgerAndWithdraw , Application } from './workflows' ; import { ethereumEnableWorkflow } from './workflows/ethereum-enable' ; import { MessagingService , MessagingServiceInterface , supportedFundingStrategyOrThrow } from './messaging' ; import { ADD_LOGS , WALLET_VERSION } from './config' ; import { logger } from './logger' ; import { ChainWatcher } from './chain' ; export class ChannelWallet { public workflows : Workflow [ ] ; param chainAddress param updateUI param setTriggerObjectiveEvent returns static async create ( chainAddress ? , updateUI ? , setTriggerObjectiveEvent ? ) { const chain = new ChainWatcher ( chainAddress ) ; const store = new Store ( chain ) ; await store . initialize ( ) ; const wallet = new ChannelWallet ( store , new MessagingService ( store ) , updateUI , setTriggerObjectiveEvent ) ; return wallet ; } constructor ( private store , private messagingService , protected updateUI ? , setTriggerObjectiveEvent ? ) { setTriggerObjectiveEvent ?. ( _ . bind ( this . crankRichObjective , this ) ) ; this . workflows = [ ] ; store . outboxFeed . subscribe ( async ( m ) => { this . messagingService . sendMessageNotification ( m ) ; } ) ; store . richObjectiveEventFeed . subscribe ( _ . bind ( this . crankRichObjective , this ) ) ; this . store . objectiveFeed . pipe ( filter ( isOpenChannel ) ) . subscribe ( async ( objective ) => { const channelEntry = await this . store . channelUpdatedFeed ( objective . data . targetChannelId ) . pipe ( take ( 1 ) ) . toPromise ( ) ; if ( this . isWorkflowIdInUse ( this . calculateWorkflowId ( objective ) ) ) { logger . warn ( `There is already a workflow running with id ${ this . calculateWorkflowId ( objective ) }, no new workflow will be spawned` ) ; } else { const fundingStrategy = supportedFundingStrategyOrThrow ( objective . data . fundingStrategy ) ; this . startWorkflow ( Application . workflow ( this . store , this . messagingService , { type : 'JOIN_CHANNEL' , fundingStrategy , channelId : objective . data . targetChannelId , applicationDomain : 'TODO' } ) , this . calculateWorkflowId ( objective ) ) ; this . messagingService . sendChannelNotification ( 'ChannelProposed' , { ... serializeChannelEntry ( channelEntry ) } ) ; } } ) ; this . store . richObjectiveFeed . subscribe ( objective => this . updateUI ?. ( { objective } ) ) ; this . messagingService . requestFeed . subscribe ( x => this . handleRequest ( x ) ) ; } private isWorkflowIdInUse ( workflowId ) { return this . workflows . map ( w => w . id ) . indexOf ( workflowId ) > - 1 ; } public getWorkflow ( workflowId ) { const workflow = this . workflows . find ( w => w . id === workflowId ) ; if ( ! workflow ) throw Error ( 'Workflow not found' ) ; return workflow ; } private calculateWorkflowId ( request ) { switch ( request . type ) { case 'JOIN_CHANNEL' : return `${ request . type }-${ request . channelId }` ; case 'OpenChannel' : return `JOIN_CHANNEL-${ request . data . targetChannelId }` ; case 'APPROVE_BUDGET_AND_FUND' : return `${ request . type }-${ request . player . participantId }-${ request . hub . participantId }` ; default : return `${ request . type }-${ Guid . create ( ) . toString ( ) }` ; } } private handleRequest ( request ) { const workflowId = this . calculateWorkflowId ( request ) ; switch ( request . type ) { case 'CREATE_CHANNEL' : { /**
                if (!this.isWorkflowIdInUse(workflowId)) {
                  this.startWorkflow(
                    Application.workflow(this.store, this.messagingService, request),
                    workflowId
                  );
                } else {
                  // TODO: To allow RPS to keep working we just warn about duplicate events
                  // Eventually this could probably be an error
                  logger.warn(
                    `There is already a workflow running with id ${workflowId}, no new workflow will be spawned`
                  );
                }
                */ this . store . createAndStoreOpenChannelObjective ( { ... request , appDefinition : makeAddress ( request . appDefinition ) } ) ; break ; } case 'JOIN_CHANNEL' : this . approveRichObjective ( request . channelId ) ; break ; case 'APPROVE_BUDGET_AND_FUND' : { const workflow = this . startWorkflow ( ApproveBudgetAndFund . machine ( this . store , this . messagingService , { player : request . player , hub : request . hub , budget : request . budget , requestId : request . requestId } ) , workflowId , true ) ; workflow . service . send ( request ) ; break ; } case 'CLOSE_AND_WITHDRAW' : { this . startWorkflow ( CloseLedgerAndWithdraw . workflow ( this . store , this . messagingService , { opponent : request . hub , player : request . player , requestId : request . requestId , domain : request . domain } ) , workflowId ) ; break ; } case 'ENABLE_ETHEREUM' : { this . startWorkflow ( ethereumEnableWorkflow ( this . store , this . messagingService , { requestId : request . requestId } ) , workflowId ) ; break ; } } } private startWorkflow ( machineConfig , workflowId , devTools = false ) { if ( this . isWorkflowIdInUse ( workflowId ) ) { throw new Error ( `There is already a workflow running with id ${ workflowId }` ) ; } const service = interpret ( machineConfig , { devTools } ) . onTransition ( ( state , event ) => ADD_LOGS && logTransition ( state , event , workflowId ) ) . onDone ( ( ) => ( this . workflows = this . workflows . filter ( w => w . id !== workflowId ) ) ) . start ( ) ; this . updateUI ?. ( { service } ) ; const workflow = { id : workflowId , service , domain : 'TODO' } ; this . workflows . push ( workflow ) ; return workflow ; } public onSendMessage ( callback ) { this . messagingService . outboxFeed . subscribe ( m => callback ( m ) ) ; } public getAddress ( ) { return this . store . getAddress ( ) ; } public async pushMessage ( jsonRpcMessage , fromDomain ) { await this . messagingService . receiveRequest ( jsonRpcMessage , fromDomain ) ; } /**
     *  START of wallet 2.0
     */ private async crankRichObjective ( event ) { const richObjective = this . store . richObjectives [ event . channelId ] ; if ( ! richObjective ) { throw new Error ( `Unable to map event ${ JSON . stringify ( event ) } to an objective` ) ; } const channelId = richObjective . channelId ; const result = DirectFunder . openChannelCranker ( richObjective , event , await this . store . getPrivateKey ( await this . store . getAddress ( ) ) ) ; this . store . richObjectives [ channelId ] = result . objective ; for ( const action of result . actions ) { switch ( action . type ) { case 'sendStates' : await Promise . all ( action . states . map ( state => this . store . addState ( state , true ) ) ) ; break ; case 'deposit' : const fundingMilestones = DirectFunder . utils . fundingMilestone ( richObjective . openingState , richObjective . openingState . participants [ richObjective . myIndex ] . destination ) ; const txHash = await this . store . chain . deposit ( channelId , fundingMilestones . targetBefore , action . amount ) ; this . crankRichObjective ( { channelId : channelId , type : 'DepositSubmitted' , tx : txHash ?? '' , attempt : 0 , submittedAt : Date . now ( ) } ) ; break ; default : throw new Error ( 'Not expected to reach here' ) ; } this . updateUI ?. ( { objective : this . store . richObjectives [ channelId ] } ) ; if ( event . type === 'Approval' ) this . sendObjective ( this . store . richObjectives [ channelId ] ) ; } } private async sendObjective ( richObjective ) { const amCreator = richObjective . myIndex === 0 ; if ( amCreator ) { const objectiveToSend = { type : 'OpenChannel' , participants : richObjective . openingState . participants , data : { targetChannelId : richObjective . channelId , fundingStrategy : 'Direct' } } ; await this . messagingService . sendMessageNotification ( { walletVersion : WALLET_VERSION , objectives : [ objectiveToSend ] } ) ; } } public async approveRichObjective ( channelId ) { this . crankRichObjective ( { type : 'Approval' , channelId : channelId } ) ; } public getRichObjectives ( ) { return this . store . richObjectives ; } /**
     * Only used for testing
     */ public destroy ( ) { this . store . chain . destroy ( ) ; } } const alreadyLogging = { } ; const key = ( v , id ) => `${ JSON . stringify ( v ) }-${ id }` ; const transitionLogger = logger . child ( { module : 'wallet' } ) ; const log = transitionLogger . trace . bind ( transitionLogger ) ; export function logTransition ( state , event , id ? ) { const k = key ( state . value , id ) ; if ( alreadyLogging [ k ] ) return ; alreadyLogging [ k ] = true ; const eventType = event . type ? event . type : event ; const { context , value : to } = state ; if ( ! state . history ) { log ( { id , workflow : state . configuration [ 0 ] . id , to , context , event } , 'WORKFLOW STARTED id %s event %s' , id , eventType ) ; } else { const from = state . history . value ; log ( { id , from , to , context , event } , 'WORKFLOW TRANSITION id %s event %o' , id , event . type ) ; } }