import { ContractArtifacts , createETHDepositTransaction , Transactions , getChallengeRegisteredEvent , ChallengeRegisteredEvent , SignedState as NitroSignedState } from '@statechannels/nitro-protocol' ; import { BN , State , SignedState , fromNitroState , toNitroSignedState , calculateChannelId , Zero , Uint256 , Address } from '@statechannels/wallet-core' ; import { Contract , Wallet , utils , providers , constants } from 'ethers' ; import { Observable , fromEvent , from , merge } from 'rxjs' ; import { filter , map , flatMap , distinctUntilChanged } from 'rxjs/operators' ; import EventEmitter from 'eventemitter3' ; import _ from 'lodash' ; import { getProvider } from './utils/contract-utils' ; import { NITRO_ADJUDICATOR_ADDRESS } from './config' ; import { logger } from './logger' ; export interface ChannelChainInfo { readonly amount : Uint256 ; readonly channelStorage : { turnNumRecord : number ; finalizesAt : number ; } ; readonly finalized : boolean ; readonly blockNum : number ; } export interface Chain { ethereumIsEnabled : boolean ; selectedAddress : string | undefined ; chainUpdatedFeed : ( channelId : string ) => Observable < ChannelChainInfo > ; challengeRegisteredFeed : ( channelId : string ) => Observable < ChallengeRegistered > ; ethereumEnable : ( ) => Promise < string > ; initialize ( ) : Promise < void > ; getBlockNumber : ( ) => Promise < number > ; deposit : ( channelId : string , expectedHeld : string , amount : string ) => Promise < string | undefined > ; challenge : ( support : SignedState [ ] , privateKey : string ) => Promise < string | undefined > ; finalizeAndWithdraw : ( finalizationProof : SignedState [ ] ) => Promise < string | undefined > ; getChainInfo : ( channelId : string ) => Promise < ChannelChainInfo > ; balanceUpdatedFeed ( address ) : Observable < Uint256 > ; /**
     * Only used for testing
     */ destroy ( ) : void ; } type Updated = ChannelChainInfo & { channelId : string ; } ; type ChallengeRegistered = { channelId : string ; challengeState : State ; challengeExpiry : number ; } ; export class FakeChain implements Chain { private blockNumber = 1 ; private channelStatus : Record < string , ChannelChainInfo > = { } ; private eventEmitter : EventEmitter < { updated : [ Updated ] ; challengeRegistered : [ ChallengeRegistered ] ; } > = new EventEmitter ( ) ; private fakeSelectedAddress : string ; public async initialize ( ) { } public async getBlockNumber ( ) { return this . blockNumber ; } public setBlockNumber ( blockNumber ) { this . blockNumber = blockNumber ; for ( const channelId in this . channelStatus ) { const { channelStorage : { finalizesAt } } = this . channelStatus [ channelId ] ; if ( finalizesAt > 0 && finalizesAt <= blockNumber ) { this . channelStatus [ channelId ] = { ... this . channelStatus [ channelId ] , finalized : true } ; this . eventEmitter . emit ( 'updated' , { channelId , ... this . channelStatus [ channelId ] } ) ; } } } public async deposit ( channelId , expectedHeld , amount ) { this . depositSync ( channelId , expectedHeld , amount ) ; return 'fake-transaction-id' ; } public async challenge ( support ) { const channelId = calculateChannelId ( support [ 0 ] ) ; const { turnNum , challengeDuration } = support [ support . length - 1 ] ; this . channelStatus [ channelId ] = { ... ( this . channelStatus [ channelId ] || { } ) , channelStorage : { turnNumRecord : turnNum , finalizesAt : this . blockNumber + challengeDuration } , finalized : challengeDuration === 0 } ; this . eventEmitter . emit ( 'updated' , { channelId , ... this . channelStatus [ channelId ] } ) ; this . eventEmitter . emit ( 'challengeRegistered' , { channelId , challengeState : support [ support . length - 1 ] , challengeExpiry : this . blockNumber + challengeDuration } ) ; return 'fake-transaction-id' ; } public async finalizeAndWithdraw ( finalizationProof ) { const channelId = calculateChannelId ( finalizationProof [ 0 ] ) ; this . finalizeSync ( channelId ) ; this . channelStatus [ channelId ] = { ... this . channelStatus [ channelId ] , amount : Zero , blockNum : this . blockNumber } ; this . eventEmitter . emit ( 'updated' , { ... this . channelStatus [ channelId ] , channelId , blockNum : this . blockNumber } ) ; return ; } public finalizeSync ( channelId , turnNum = 0 ) { this . channelStatus [ channelId ] = { ... ( this . channelStatus [ channelId ] || { } ) , channelStorage : { turnNumRecord : turnNum , finalizesAt : this . blockNumber } } ; } public depositSync ( channelId , expectedHeld , amount ) { const current = ( this . channelStatus [ channelId ] || { } ) . amount || Zero ; if ( BN . gte ( current , expectedHeld ) ) { this . channelStatus [ channelId ] = { ... this . channelStatus [ channelId ] , amount : BN . add ( current , amount ) } ; this . eventEmitter . emit ( 'updated' , { ... this . channelStatus [ channelId ] , channelId } ) ; } } public async getChainInfo ( channelId ) { const { amount , channelStorage } = this . channelStatus [ channelId ] || { } ; return { channelStorage : channelStorage || { turnNumRecord : 0 , finalizesAt : 0 } , finalized : channelStorage && channelStorage . finalizesAt > 0 && channelStorage . finalizesAt <= this . blockNumber , blockNum : this . blockNumber , amount : amount || Zero } ; } public chainUpdatedFeed ( channelId ) { const first = from ( this . getChainInfo ( channelId ) ) ; const updates = fromEvent ( this . eventEmitter , 'updated' ) . pipe ( filter ( ( event ) => event . channelId === channelId ) , map ( ( { amount , channelStorage , finalized , blockNum : blockNum } ) => ( { amount , channelStorage , finalized , blockNum } ) ) ) ; return merge ( first , updates ) ; } public balanceUpdatedFeed ( ) { return from ( [ BN . from ( '0x999999999999' ) ] ) ; } public challengeRegisteredFeed ( channelId ) { const updates = fromEvent ( this . eventEmitter , 'challengeRegistered' ) . pipe ( filter ( ( event ) => event . channelId === channelId ) , map ( ( { challengeState , challengeExpiry } ) => ( { channelId , challengeState , challengeExpiry } ) ) ) ; return merge ( updates ) ; } public ethereumEnable ( ) { this . fakeSelectedAddress = utils . hexZeroPad ( '0x123' , 32 ) ; return Promise . resolve ( this . selectedAddress ) ; } public get ethereumIsEnabled ( ) { return true ; } public get selectedAddress ( ) { return this . fakeSelectedAddress ; } public destroy ( ) { _ . noop ( ) ; } } const chainLogger = logger . child ( { module : 'chain' } ) ; const GAS_PRICE = utils . parseUnits ( '15' , 'gwei' ) ; export class ChainWatcher implements Chain { private _adjudicator ? : Contract ; private provider : ReturnType < typeof getProvider > ; private mySelectedAddress : string | undefined ; constructor ( chainAddress ? ) { this . mySelectedAddress = chainAddress ?? window . ethereum ?. selectedAddress ?? undefined ; } private get signer ( ) { if ( ! this . ethereumIsEnabled ) throw new Error ( 'Ethereum not enabled' ) ; if ( window . ethereum ?. mockingInfuraProvider ) { return new Wallet ( '0xccb052837ccafb700e34c0e0cc0f3e5fbee8f078f3fe6b4e5950c7c8acaa7bce' , this . provider ) ; } return this . provider . getSigner ( this . selectedAddress as string ) ; } public async initialize ( ) { this . provider = getProvider ( ) ; this . provider . on ( 'block' , blockNumber => chainLogger . trace ( { blockNumber } , 'New Block' ) ) ; this . configureContracts ( ) ; } private configureContracts ( ) { if ( ! this . ethereumIsEnabled ) return ; this . _adjudicator = new Contract ( NITRO_ADJUDICATOR_ADDRESS , ContractArtifacts . NitroAdjudicatorArtifact . abi , this . signer ) ; chainLogger . info ( { NITRO_ADJUDICATOR_ADDRESS } , 'Contracts configured' ) ; } public async getBlockNumber ( ) { return this . provider . getBlockNumber ( ) ; } public async ethereumEnable ( ) { if ( window . ethereum ) { try { this . mySelectedAddress = ( await window . ethereum . enable ( ) ) [ 0 ] ; if ( this . ethereumIsEnabled ) { this . configureContracts ( ) ; return this . selectedAddress as string ; } else { const error = 'Ethereum enabled but no selected address is defined' ; chainLogger . error ( error ) ; return Promise . reject ( error ) ; } } catch ( error ) { chainLogger . error ( error ) ; return Promise . reject ( 'user rejected in metamask' ) ; } } else { return Promise . reject ( 'window.ethereum not found' ) ; } } public get selectedAddress ( ) : string | undefined { if ( this . mySelectedAddress === null ) { if ( window && window . ethereum ) { this . mySelectedAddress = window . ethereum . selectedAddress ?? null ; } } return this . mySelectedAddress ; } public get ethereumIsEnabled ( ) : boolean { return typeof this . selectedAddress === 'string' ; } public async finalizeAndWithdraw ( finalizationProof ) { const transactionRequest = { ... Transactions . createConcludeAndTransferAllAssetsTransaction ( finalizationProof . flatMap ( toNitroSignedState ) ) , to : NITRO_ADJUDICATOR_ADDRESS } ; const response = await this . signer . sendTransaction ( { ... convertNitroTransactionRequest ( transactionRequest ) , gasPrice : GAS_PRICE } ) ; return response . hash ; } public async challenge ( support , privateKey ) { const convertedSignedStates = support . reduce ( ( previous , current ) => previous . concat ( toNitroSignedState ( current ) ) , new Array < NitroSignedState > ( ) ) . sort ( ( s1 , s2 ) => s1 . state . turnNum - s2 . state . turnNum ) ; const transactionRequest = { ... Transactions . createChallengeTransaction ( convertedSignedStates , privateKey ) , to : NITRO_ADJUDICATOR_ADDRESS } ; const response = await this . signer . sendTransaction ( { ... convertNitroTransactionRequest ( transactionRequest ) , gasPrice : GAS_PRICE } ) ; const tx = await response . wait ( ) ; return tx . transactionHash ; } public async deposit ( channelId , expectedHeld , amount ) { const transactionRequest = { ... createETHDepositTransaction ( channelId , expectedHeld , amount ) , to : NITRO_ADJUDICATOR_ADDRESS , value : amount } ; const response = await this . signer . sendTransaction ( { ... convertNitroTransactionRequest ( transactionRequest ) , gasPrice : GAS_PRICE } ) ; chainLogger . trace ( { response } , 'Deposit successful from %s' , response . from ) ; return response . hash ; } public async getChainInfo ( channelId ) { if ( ! this . _adjudicator ) { throw new Error ( 'Not connected to contracts' ) ; } const amount = BN . from ( await this . _adjudicator . holdings ( constants . AddressZero , channelId ) ) ; const [ turnNumRecord , finalizesAt ] = await this . _adjudicator . unpackStatus ( channelId ) ; const blockNum = await this . provider . getBlockNumber ( ) ; chainLogger . trace ( { amount , channelStorage : { turnNumRecord , finalizesAt } , finalized : BN . gt ( finalizesAt , 0 ) && BN . lte ( finalizesAt , blockNum ) , blockNum } , 'Chain query result' ) ; return { amount , channelStorage : { turnNumRecord , finalizesAt } , finalized : BN . gt ( finalizesAt , 0 ) && BN . lte ( finalizesAt , blockNum ) , blockNum } ; } public balanceUpdatedFeed ( address ) { const first = from ( this . provider . getBalance ( address ) . then ( BN . from ) ) ; const updates = fromEvent < Uint256 > ( this . provider , 'block' ) . pipe ( flatMap ( ( ) => this . provider . getBalance ( address ) ) ) ; return merge ( first , updates ) . pipe ( distinctUntilChanged < Uint256 > ( BN . eq ) ) ; } public chainUpdatedFeed ( channelId ) { if ( ! this . _adjudicator ) { throw new Error ( 'Not connected to contracts' ) ; } const depositEvents = fromEvent ( this . _adjudicator , 'Deposited' ) . pipe ( filter ( ( event ) => BN . eq ( event [ 0 ] , channelId ) ) , flatMap ( async ( event ) => ( { ... ( await this . getChainInfo ( channelId ) ) , amount : BN . from ( event . slice ( - 1 ) [ 0 ] . args . destinationHoldings ) } ) ) ) ; const assetTransferEvents = fromEvent ( this . _adjudicator , 'AllocationUpdated' ) . pipe ( filter ( ( event ) => BN . eq ( event [ 0 ] , channelId ) ) , flatMap ( async ( ) => this . getChainInfo ( channelId ) ) ) ; return merge ( depositEvents , assetTransferEvents ) ; } public challengeRegisteredFeed ( channelId ) { if ( ! this . _adjudicator ) { throw new Error ( 'Not connected to contracts' ) ; } const updates = fromEvent ( this . _adjudicator , 'ChallengeRegistered' ) . pipe ( filter ( ( event ) => event [ 0 ] === channelId ) , map ( getChallengeRegisteredEvent ) , map ( ( { challengeStates , finalizesAt } ) => ( { channelId , challengeState : fromNitroState ( challengeStates [ challengeStates . length - 1 ] . state ) , challengeExpiry : finalizesAt } ) ) ) ; return merge ( updates ) ; } /**
     * Only used for testing
     */ public destroy ( ) { this . provider . polling = false ; this . _adjudicator ?. removeAllListeners ( ) ; this . provider . removeAllListeners ( ) ; } } function convertNitroTransactionRequest ( nitroTransactionRequest ) { return { ... nitroTransactionRequest , gasLimit : nitroTransactionRequest . gasLimit ? BN . from ( nitroTransactionRequest . gasLimit ) : undefined , gasPrice : nitroTransactionRequest . gasPrice ? BN . from ( nitroTransactionRequest . gasPrice ) : undefined , nonce : nitroTransactionRequest . nonce ? BN . from ( nitroTransactionRequest . nonce ) : undefined , value : nitroTransactionRequest . value ? BN . from ( nitroTransactionRequest . value ) : undefined } ; }