import { BigNumber , ONE , SignatureProvider , ZERO } from '@pigi/core-utils' ; import uuid = require ( 'uuid' ) ; import { AddressBalance , deserializeMessage , deserializeMessageString , messageToString , parseStateChannelSignedMessage , StateChannelExitClaim , StateChannelMessage , stateChannelMessageDeserializer , stateChannelMessageToString , } from '../serialization' ; import { AndDecider , CannotDecideError , ForAllSuchThatDecider , MessageNonceLessThanDecider , Utils , SignedByDecider , } from '../deciders' ; import { SignedByQuantifier } from '../quantifiers' ; import { StateChannelMessageDBInterface } from '../../types/db' ; import { ParsedMessage , SignedMessage } from '../../types/serialization' ; import { Decision , ImplicationProofItem } from '../../types' ; /**
 * Client responsible for State Channel communication
 */ export class StateChannelClient { public constructor ( private readonly messageDB , private readonly signedByDecider , private readonly signedByQuantifier , public readonly myAddress , public readonly signatureProvider ) { } param addressBalance param recipient returns public async createNewMessage ( addressBalance , recipient ) { let channelID = await this . messageDB . getChannelForCounterparty ( recipient ) ; let nonce ; if ( ! ! channelID ) { const [ lastValid , lastSigned , exited ] = await Promise . all ( [ this . messageDB . getMostRecentValidStateChannelMessage ( channelID ) , this . messageDB . getMostRecentMessageSignedBy ( channelID , this . myAddress ) , this . messageDB . isChannelExited ( channelID ) , ] ) ; if ( ( ! lastSigned && ! lastValid ) || ! lastSigned . message . nonce . equals ( lastValid . message . nonce ) ) { throw Error ( 'Cannot create new message when last message is not counter-signed' ) ; } if ( exited ) { throw Error ( 'Cannot create new message for exited channel.' ) ; } nonce = lastValid . message . nonce . add ( ONE ) ; } else { channelID = uuid . v4 ( ) ; nonce = ONE ; } return this . signAndSaveMessage ( { sender : this . myAddress , recipient , message : { channelID , nonce , data : { addressBalance } , } , signatures : { } , } ) ; } param counterparty returns public async exitChannel ( counterparty ) { const channelID = await this . messageDB . getChannelForCounterparty ( counterparty ) ; if ( ! channelID ) { throw Error ( 'Cannot exit a channel that does not exist.' ) ; } const mostRecent = await this . messageDB . getMostRecentValidStateChannelMessage ( channelID ) ; await this . messageDB . markChannelExited ( channelID ) ; return { decider : AndDecider . instance ( ) , input : { properties : [ { decider : this . signedByDecider , input : { serializedMessage : messageToString ( mostRecent . message , stateChannelMessageToString ) , publicKey : counterparty , } , witness : { signature : mostRecent . signatures [ counterparty . toString ( ) ] , } , } , { decider : ForAllSuchThatDecider . instance ( ) , input : { quantifier : this . signedByQuantifier , quantifierParameters : { address : this . myAddress } , propertyFactory : ( signedMessage ) => { return { decider : MessageNonceLessThanDecider . instance ( ) , input : { messageWithNonce : deserializeMessageString ( signedMessage . serializedMessage , deserializeMessage , stateChannelMessageDeserializer ) , lessThanThis : mostRecent . message . nonce . add ( ONE ) , } , } ; } , } , } , ] , } , } ; } param channelID param exitClaim returns public async handleChannelExit ( channelID , exitClaim ) { let decision ; try { decision = await exitClaim . decider . decide ( exitClaim . input ) ; } catch ( e ) { if ( ! ( e instanceof CannotDecideError ) ) { throw e ; } } if ( ! decision || decision . outcome ) { await this . messageDB . markChannelExited ( channelID ) ; return undefined ; } return decision . justification ; } param message returns public async handleMessage ( message ) { const parsedMessage = await parseStateChannelSignedMessage ( message , this . myAddress ) ; const existingMessage = await this . messageDB . getMessageByChannelIdAndNonce ( parsedMessage . message . channelID , parsedMessage . message . nonce ) ; const mergedMessage = this . updateWithReceived ( existingMessage , parsedMessage ) ; await this . messageDB . storeMessage ( mergedMessage || parsedMessage ) ; if ( ! ! existingMessage ) { return undefined ; } if ( parsedMessage . message . nonce . equals ( ONE ) ) { return this . handleNewChannel ( parsedMessage ) ; } return this . handleNewMessage ( parsedMessage ) ; } param message return private async handleNewChannel ( message ) { if ( ! this . validateStateChannelMessage ( message ) ) { return undefined ; } return this . signAndSaveMessage ( message ) ; } param message returns private async handleNewMessage ( message ) { if ( ! this . validateStateChannelMessage ( message ) ) { return undefined ; } const [ exited , conflicts , previousMessage ] = await Promise . all ( [ this . messageDB . isChannelExited ( message . message . channelID ) , this . messageDB . conflictsWithAnotherMessage ( message ) , this . messageDB . getMostRecentValidStateChannelMessage ( message . message . channelID ) , ] ) ; if ( ! ! conflicts || exited ) { return undefined ; } if ( ! previousMessage || previousMessage . message . nonce . gte ( message . message . nonce ) ) { return undefined ; } return this . signAndSaveMessage ( message ) ; } param existing param received returns private updateWithReceived ( existing , received ) { if ( ! ! existing && ! Utils . stateChannelMessagesConflict ( received , existing ) ) { for ( const [ address , signature ] of Object . entries ( received . signatures ) ) { existing . signatures [ address ] = signature ; } return existing ; } return undefined ; } param message returns private async signAndSaveMessage ( message ) { const serializedMessage = messageToString ( message . message , stateChannelMessageToString ) ; const signature = await this . signatureProvider . sign ( serializedMessage ) ; message . signatures [ this . myAddress ] = signature ; await this . messageDB . storeMessage ( message ) ; return { signature , serializedMessage , } ; } param message returns private validateStateChannelMessage ( message ) { try { const stateChannelMessage = message . message . data as StateChannelMessage ; return ( message . message . nonce . gte ( ZERO ) && Object . keys ( stateChannelMessage . addressBalance ) . length === 2 && stateChannelMessage . addressBalance [ this . myAddress . toString ( ) ] . gte ( ZERO ) && stateChannelMessage . addressBalance [ message . sender . toString ( ) ] . gte ( ZERO ) ) ; } catch ( e ) { return false ; } } }