import { interpret } from 'xstate' ; import waitForExpect from 'wait-for-expect' ; import { checkThat , isSimpleEthAllocation , firstState , calculateChannelId , createSignatureEntry , ChannelConstants , Outcome , State , SignedState , BN } from '@statechannels/wallet-core' ; import _ from 'lodash' ; import { FakeChain , Chain } from '../../chain' ; import { TestStore } from '../../test-store' ; import { Init , machine , Errors } from '../ledger-funding' ; import { zeroAddress } from '../../config' ; import { wallet1 , wallet2 , participants } from './data' ; import { subscribeToMessages } from './message-service' ; jest . setTimeout ( 10000 ) ; const { add } = BN ; const EXPECT_TIMEOUT = process . env . CI ? 9500 : 2000 ; const chainId = '0x01' ; const challengeDuration = 10 ; const appDefinition = zeroAddress ; const targetChannel = { channelNonce : 0 , chainId , challengeDuration , participants , appDefinition } ; const targetChannelId = calculateChannelId ( targetChannel ) ; const ledgerChannel = { channelNonce : 1 , chainId , challengeDuration , participants , appDefinition } ; const ledgerChannelId = calculateChannelId ( ledgerChannel ) ; const destinations = participants . map ( p => p . destination ) ; const amounts = [ BN . from ( 7 ) , BN . from ( 5 ) ] ; const deductionAmounts = [ BN . from ( 3 ) , BN . from ( 2 ) ] ; const outcome = { type : 'SimpleAllocation' , asset : zeroAddress , allocationItems : [ 0 , 1 ] . map ( i => ( { destination : destinations [ i ] , amount : amounts [ i ] } ) ) } ; const deductions = [ 0 , 1 ] . map ( i => ( { destination : destinations [ i ] , amount : deductionAmounts [ i ] } ) ) ; const context = { targetChannelId , ledgerChannelId , deductions } ; let chain ; let aStore ; let bStore ; const allSignState = ( state ) => ( { ... state , signatures : [ wallet1 , wallet2 ] . map ( ( { privateKey } ) => createSignatureEntry ( state , privateKey ) ) } ) ; describe ( 'success' , ( ) => { beforeEach ( async ( ) => { aStore = new TestStore ( chain ) ; await aStore . initialize ( [ wallet1 . privateKey ] ) ; bStore = new TestStore ( chain ) ; await bStore . initialize ( [ wallet2 . privateKey ] ) ; await Promise . all ( [ aStore , bStore ] . map ( async ( store ) => { await store . createEntry ( allSignState ( firstState ( outcome , targetChannel ) ) ) ; await store . setLedgerByEntry ( await store . createEntry ( allSignState ( firstState ( outcome , ledgerChannel ) ) ) ) ; } ) ) ; subscribeToMessages ( { [ participants [ 0 ] . participantId ] : aStore , [ participants [ 1 ] . participantId ] : bStore } ) ; } ) ; test ( 'happy path' , async ( ) => { const _chain = new FakeChain ( ) ; _chain . depositSync ( ledgerChannelId , '0' , amounts . reduce ( add ) ) ; [ aStore , bStore ] . forEach ( ( store ) => ( ( store as any ) . chain = _chain ) ) ; const aService = interpret ( machine ( aStore ) . withContext ( context ) ) ; const bService = interpret ( machine ( bStore ) . withContext ( context ) ) ; [ aService , bService ] . map ( s => s . start ( ) ) ; await waitForExpect ( async ( ) => { expect ( bService . state . value ) . toEqual ( 'success' ) ; expect ( aService . state . value ) . toEqual ( 'success' ) ; const { supported : supportedState } = await aStore . getEntry ( ledgerChannelId ) ; const outcome = checkThat ( supportedState . outcome , isSimpleEthAllocation ) ; expect ( outcome . allocationItems ) . toMatchObject ( [ 0 , 1 ] . map ( i => ( { destination : destinations [ i ] , amount : BN . sub ( amounts [ i ] , deductionAmounts [ i ] ) } ) ) . concat ( [ { destination : targetChannelId as any , amount : deductionAmounts . reduce ( add ) } ] ) ) ; expect ( ( await aStore . getEntry ( targetChannelId ) ) . funding ) . toMatchObject ( { type : 'Indirect' , ledgerId : ledgerChannelId } ) ; } , EXPECT_TIMEOUT ) ; } ) ; test ( 'locks' , async ( ) => { const _chain = new FakeChain ( ) ; _chain . depositSync ( ledgerChannelId , '0' , amounts . reduce ( add ) ) ; [ aStore , bStore ] . forEach ( ( store ) => ( ( store as any ) . chain = _chain ) ) ; const aService = interpret ( machine ( aStore ) . withContext ( context ) ) ; const bService = interpret ( machine ( bStore ) . withContext ( context ) ) ; const { release } = await bStore . acquireChannelLock ( context . ledgerChannelId ) ; [ aService , bService ] . map ( s => s . start ( ) ) ; await waitForExpect ( async ( ) => { expect ( bService . state . value ) . toEqual ( 'acquiringLock' ) ; expect ( aService . state . value ) . toEqual ( { fundingTarget : 'supportState' } ) ; } , EXPECT_TIMEOUT ) ; aService . onTransition ( s => { if ( _ . isEqual ( s . value , { fundTarget : 'getTargetOutcome' } ) ) { expect ( ( s . context as any ) . lock ) . toBeDefined ( ) ; } } ) ; release ( ) ; await waitForExpect ( async ( ) => { expect ( aService . state . value ) . toEqual ( 'success' ) ; } , EXPECT_TIMEOUT ) ; } ) ; } ) ; describe ( 'failure modes' , ( ) => { type Data = { ledgerOutcome ? : Outcome ; initialTargetState ? : SignedState ; chainInfo ? : { amount : string ; finalized : boolean ; } ; } ; const oneWeiDeposited = { ledgerOutcome : outcome , chainInfo : { amount : '1' , finalized : false } } ; const fiveTotalAllocated = { ledgerOutcome : { type : 'SimpleAllocation' , asset : zeroAddress , allocationItems : [ 0 , 1 ] . map ( i => ( { destination : destinations [ i ] , amount : BN . sub ( deductionAmounts [ i ] , 1 ) } ) ) } } ; const finalizedLedger = { chainInfo : { finalized : true , amount : '12' } } ; const unsupportedTarget = { initialTargetState : { ... firstState ( outcome , targetChannel ) , signatures : [ ] } } ; test . each `
    description             | data                  | error
    ${ 'underfunded' }        | ${ oneWeiDeposited }    | ${ Errors . underfunded }
    ${ 'underallocated' }     | ${ fiveTotalAllocated } | ${ Errors . underallocated }
    ${ 'finalized' }          | ${ finalizedLedger }    | ${ Errors . finalized }
    ${ 'unsupported target' } | ${ unsupportedTarget }  | ${ Errors . unSupportedTargetChannel + targetChannelId }
  ` ( 'failure mode: $description' , async ( { error , data } ) => { const ledgerOutcome = data . ledgerOutcome ?? outcome ; const initialTargetState = data . initialTargetState ?? allSignState ( firstState ( outcome , targetChannel ) ) ; const chainInfo = data . chainInfo ?? { amount : '12' , finalized : false } ; aStore = new TestStore ( chain ) ; await aStore . initialize ( [ wallet1 . privateKey ] ) ; bStore = new TestStore ( chain ) ; await bStore . initialize ( [ wallet2 . privateKey ] ) ; const initialLedgerState = allSignState ( firstState ( ledgerOutcome , ledgerChannel ) ) ; await Promise . all ( [ aStore , bStore ] . map ( async ( store ) => { await store . createEntry ( initialTargetState ) ; await store . setLedgerByEntry ( await store . createEntry ( initialLedgerState ) ) ; } ) ) ; subscribeToMessages ( { [ participants [ 0 ] . participantId ] : aStore , [ participants [ 1 ] . participantId ] : bStore } ) ; const _chain = new FakeChain ( ) ; _chain . depositSync ( ledgerChannelId , '0' , chainInfo . amount ) ; chainInfo . finalized && _chain . finalizeSync ( ledgerChannelId ) ; ( aStore as any ) . chain = _chain ; aStore . createEntry ( allSignState ( { ... firstState ( ledgerOutcome , ledgerChannel ) , turnNum : 1 } ) ) ; aStore . chain . initialize ( ) ; const aService = interpret ( machine ( aStore ) . withContext ( context ) , { parent : { send : ( ) => undefined } as any } ) . start ( ) ; await waitForExpect ( async ( ) => { expect ( aService . state . value ) . toEqual ( 'failure' ) ; expect ( aService . state . context ) . toMatchObject ( { error } ) ; } , EXPECT_TIMEOUT ) ; } ) ; } ) ;