import _ from 'lodash' ; import { FundingStrategy } from '@statechannels/client-api-schema' ; import { BN , calculateChannelId , ChannelConstants , makeAddress , makeDestination , makePrivateKey , Participant , PrivateKey , serializeState , SignedStateWithHash , SimpleAllocation , simpleEthAllocation , State , NULL_APP_DATA , CloseChannel , DefundChannel , OpenChannel , Destination , } from '@statechannels/wallet-core' ; import { SignedState as WireState , Payload } from '@statechannels/wire-format' ; import { constants , utils } from 'ethers' ; import { Channel } from '../../../models/channel' ; import { defaultTestWalletConfig } from '../../../config' ; import { WalletObjective , ObjectiveModel } from '../../../models/objective' ; import { SigningWallet } from '../../../models/signing-wallet' ; import { WALLET_VERSION } from '../../../version' ; import { Store } from '../../store' ; import { Bytes32 } from '../../../type-aliases' ; import { alice , bob } from './participants' ; import { alice as aliceWallet , bob as bobWallet } from './signing-wallets' ; import { stateWithHashSignedBy } from './states' ; param finalFrom interface TestChannelArgs { aBal ? : number ; bBal ? : number ; finalFrom ? : number ; fundingStrategy ? : FundingStrategy ; fundingLedgerChannelId ? : Bytes32 ; } export type Bals = [ string , number ] [ ] | [ number , number ] ; /** A two-party channel between Alice and Bob, with state history. For testing purposes. */ export class TestChannel { public participantA : Participant = alice ( ) ; public participantB : Participant = bob ( ) ; public signingWalletA : SigningWallet = aliceWallet ( ) ; public signingWalletB : SigningWallet = bobWallet ( ) ; public startBals : Bals ; public channelNonce : number ; public finalFrom ? : number ; public fundingStrategy : FundingStrategy ; public fundingLedgerChannelId : Bytes32 | undefined ; static maximumNonce = 0 ; public asset = makeAddress ( constants . AddressZero ) ; public get participants ( ) : Participant [ ] { return [ this . participantA , this . participantB ] ; } public get signingWallets ( ) : SigningWallet [ ] { return [ this . signingWalletA , this . signingWalletB ] ; } public static create ( args ) { return new TestChannel ( args ) ; } protected constructor ( args ) { this . fundingLedgerChannelId = args . fundingLedgerChannelId ; this . fundingStrategy = args . fundingStrategy || ( args . fundingLedgerChannelId ? 'Ledger' : 'Direct' ) ; this . startBals = [ [ 'a' , args . aBal ?? 5 ] , [ 'b' , args . bBal ?? 5 ] , ] ; this . finalFrom = args . finalFrom ; this . channelNonce = TestChannel . maximumNonce ++ ; } param n public state ( n , bals ? ) { if ( n < 2 ) { n = 0 ; } else if ( n < 4 ) { n = 3 ; } else if ( this . finalFrom && n > this . finalFrom ) { n = this . finalFrom ; } return { ... this . channelConstants , appData : NULL_APP_DATA , isFinal : ! ! this . finalFrom && n >= this . finalFrom , turnNum : n , outcome : bals ? this . toOutcome ( bals ) : this . startOutcome , } ; } public signedStateWithHash ( n , bals ? , signedBy ? ) { if ( _ . isUndefined ( signedBy ) ) signedBy = n % 2 === 0 ? 0 : 1 ; const signers = signedBy === 'both' ? [ 0 , 1 ] : [ signedBy ] ; return stateWithHashSignedBy ( signers . map ( x => this . signingWallets [ x ] ) ) ( this . state ( n , bals ) ) ; } /**
     * Gives the nth state in the history, signed by the correct participant
     */ public wireState ( n , bals ? , signedBy ? ) { return serializeState ( this . signedStateWithHash ( n , bals , signedBy ) ) ; } public wirePayload ( n , bals ? , signedBy ? ) { return { walletVersion : WALLET_VERSION , signedStates : [ this . wireState ( n , bals , signedBy ) ] , } ; } public get startOutcome ( ) : SimpleAllocation { return this . toOutcome ( this . startBals ) ; } public get isLedger ( ) : boolean { return false ; } public toOutcome ( bals ) { if ( typeof bals [ 0 ] === 'number' ) { bals = bals as [ number , number ] ; bals = [ [ 'a' , bals [ 0 ] ] , [ 'b' , bals [ 1 ] ] , ] ; } bals = bals as [ string , number ] [ ] ; return simpleEthAllocation ( bals . map ( ( [ dest , amt ] ) => { const amount = BN . from ( amt ) ; if ( dest === 'a' ) { return { destination : this . participantA . destination , amount } ; } else if ( dest === 'b' ) { return { destination : this . participantB . destination , amount } ; } else { return { destination : makeDestination ( dest ) , amount : BN . from ( amt ) } ; } } ) ) ; } public get signingKeyA ( ) : PrivateKey { return makePrivateKey ( this . signingWalletA . privateKey ) ; } public get signingKeyB ( ) : PrivateKey { return makePrivateKey ( this . signingWalletB . privateKey ) ; } public get signingKeys ( ) : PrivateKey [ ] { return [ this . signingKeyA , this . signingKeyB ] ; } public get channelConstants ( ) : ChannelConstants { return { appDefinition : makeAddress ( '0x000000000000000000000000000000000000adef' ) , participants : this . participants , channelNonce : this . channelNonce , chainId : utils . hexlify ( defaultTestWalletConfig ( ) . networkConfiguration . chainNetworkID ) , challengeDuration : 9001 , } ; } public get channelId ( ) : Destination { return makeDestination ( calculateChannelId ( this . channelConstants ) ) ; } public get openChannelObjective ( ) : OpenChannel { return { participants : this . participants , type : 'OpenChannel' , data : { targetChannelId : this . channelId , fundingStrategy : this . fundingStrategy , fundingLedgerChannelId : this . fundingLedgerChannelId , } , } ; } public closeChannelObjective ( txSubmitterOrder = [ 0 , 1 ] ) { return { participants : this . participants , type : 'CloseChannel' , data : { targetChannelId : this . channelId , fundingStrategy : this . fundingStrategy , txSubmitterOrder , } , } ; } public defundChannelObjective ( ) { return { participants : this . participants , type : 'DefundChannel' , data : { targetChannelId : this . channelId , } , } ; } public get openChannelPayload ( ) : Payload { return { walletVersion : WALLET_VERSION , signedStates : [ this . wireState ( 0 ) ] , objectives : [ this . openChannelObjective ] , } ; } public get getChannelRequest ( ) : Payload { return { walletVersion : WALLET_VERSION , requests : [ { channelId : this . channelId , type : 'GetChannel' } ] , } ; } static mergePayloads ( payload1 , payload2 ) { return { walletVersion : payload1 . walletVersion , signedStates : combineArrays ( payload1 . signedStates , payload2 . signedStates ) , requests : combineArrays ( payload1 . requests , payload2 . requests ) , objectives : combineArrays ( payload1 . objectives , payload2 . objectives ) , } ; } static get emptyPayload ( ) : Payload { return { walletVersion : WALLET_VERSION } ; } public get startBal ( ) : number { if ( typeof this . startBals [ 0 ] === 'number' ) { return ( this . startBals as [ number , number ] ) . reduce ( ( sum , amt ) => sum + amt ) ; } else { return ( this . startBals as [ string , number ] [ ] ) . reduce ( ( sum , [ _dest , amt ] ) => sum + amt , 0 ) ; } } public async insertState ( store , state , bals ? ) { if ( typeof state === 'number' ) { await store . pushMessage ( this . wirePayload ( Number ( state ) , bals ) ) ; return Number ( state ) ; } else { await store . pushMessage ( this . wirePayload ( Number ( state . turn ) , state . bals , state . signedBy ) ) ; return Number ( state . turn ) ; } } /**
     * Calls addSigningKey, pushMessage, updateFunding, on the supplied store, patches the fundingStrategy, and adds the OpenChannel objective
     */ public async insertInto ( store , args = { } ) { const { states , participant , bals } = { states : [ 0 ] , participant : 0 , ... args } ; await store . addSigningKey ( this . signingKeys [ participant ] ) ; const turnNums = [ ] as number [ ] ; const [ first , ... rest ] = states ; const turnNum = await this . insertState ( store , first , bals ) ; turnNums . push ( turnNum ) ; const objective = await ObjectiveModel . insert ( this . openChannelObjective , store . knex , 'approved' ) ; if ( this . isLedger ) { await Channel . setLedger ( this . channelId , store . knex ) ; } const { fundingStrategy , fundingLedgerChannelId } = objective . data ; await Channel . query ( store . knex ) . where ( { channelId : this . channelId } ) . patch ( { fundingStrategy , fundingLedgerChannelId } ) ; for ( const state of rest ) { const turnNum = await this . insertState ( store , state , bals ) ; turnNums . push ( turnNum ) ; } const funds = args . funds !== undefined ? args . funds : Math . max ( ... turnNums ) > 1 ? this . startBal : 0 ; if ( funds > 0 ) { await store . updateFunding ( this . channelId , BN . from ( funds ) , this . asset ) ; } await Channel . query ( store . knex ) . where ( { channelId : this . channelId } ) . patch ( { fundingStrategy : this . fundingStrategy , } ) ; return objective ; } } export interface InsertionParams { participant ? : 0 | 1 ; states ? : number [ ] | StateWithBals [ ] ; funds ? : number ; bals ? : Bals ; } export type SignedBy = 0 | 1 | 'both' ; export interface StateWithBals { turn : number ; bals : Bals ; signedBy : SignedBy ; } function combineArrays < T > ( a1 , a2 ) { const result = [ ... ( a1 || [ ] ) , ... ( a2 || [ ] ) ] ; if ( result . length > 0 ) { return result ; } else { return undefined ; } }