import IChannelsDatabase from './storage/IChannelsDatabase' ; import PaymentManager from './PaymentManager' ; import ChannelContract from './ChannelContract' ; import { TransactionResult } from 'truffle-contract' ; import Mutex from './Mutex' ; import Payment from './payment' ; import MachinomyOptions from './MachinomyOptions' ; import IChannelManager from './IChannelManager' ; import * as BigNumber from 'bignumber.js' ; import ChannelId from './ChannelId' ; import { EventEmitter } from 'events' ; import * as Web3 from 'web3' ; import IPaymentsDatabase from './storage/IPaymentsDatabase' ; import ITokensDatabase from './storage/ITokensDatabase' ; import Logger from '@machinomy/logger' ; import { PaymentChannel } from './PaymentChannel' ; import ChannelInflator from './ChannelInflator' ; import * as uuid from 'uuid' ; import { PaymentNotValidError , InvalidChannelError } from './Exceptions' ; import { RemoteChannelInfo } from './RemoteChannelInfo' ; import { recoverPersonalSignature } from 'eth-sig-util' ; import pify from './util/pify' ; const LOG = new Logger ( 'channel-manager' ) ; const DAY_IN_SECONDS = 86400 ; const NEW_BLOCK_TIME_IN_SECONDS = 15 ; export default class ChannelManager extends EventEmitter implements IChannelManager { /** Default settlement period for a payment channel */ static DEFAULT_SETTLEMENT_PERIOD = 2 * DAY_IN_SECONDS / NEW_BLOCK_TIME_IN_SECONDS ; private account : string ; private web3 : Web3 ; private channelsDao : IChannelsDatabase ; private paymentsDao : IPaymentsDatabase ; private tokensDao : ITokensDatabase ; private channelContract : ChannelContract ; private paymentManager : PaymentManager ; private mutex : Mutex = new Mutex ( ) ; private machinomyOptions : MachinomyOptions ; constructor ( account , web3 , channelsDao , paymentsDao , tokensDao , channelContract , paymentManager , machinomyOptions ) { super ( ) ; this . account = account ; this . web3 = web3 ; this . channelsDao = channelsDao ; this . paymentsDao = paymentsDao ; this . tokensDao = tokensDao ; this . channelContract = channelContract ; this . paymentManager = paymentManager ; this . machinomyOptions = machinomyOptions ; } openChannel ( sender , receiver , amount , minDepositAmount ? , channelId ? , tokenContract ? ) { return this . mutex . synchronize ( async ( ) => { return this . internalOpenChannel ( sender , receiver , amount , minDepositAmount , channelId , tokenContract ) ; } ) ; } closeChannel ( channelId ) { return this . mutex . synchronizeOn ( channelId . toString ( ) , ( ) => this . internalCloseChannel ( channelId ) ) ; } deposit ( channelId , value ) { return this . mutex . synchronizeOn ( channelId , async ( ) => { const channel = await this . channelById ( channelId ) ; if ( ! channel ) { throw new Error ( 'No payment channel found.' ) ; } const res = await this . channelContract . deposit ( this . account , channelId , value , channel . tokenContract ) ; await this . channelsDao . deposit ( channelId , value ) ; return res ; } ) ; } nextPayment ( channelId , amount , meta ) { return this . mutex . synchronizeOn ( channelId . toString ( ) , async ( ) => { const channel = await this . channelById ( channelId ) ; if ( ! channel ) { throw new Error ( `Channel with id ${ channelId . toString ( ) } not found.` ) ; } const toSpend = channel . spent . add ( amount ) ; if ( toSpend . greaterThan ( channel . value ) ) { throw new Error ( `Total spend ${ toSpend . toString ( ) } is larger than channel value ${ channel . value . toString ( ) }` ) ; } return this . paymentManager . buildPaymentForChannel ( channel , amount , toSpend , meta ) ; } ) ; } async spendChannel ( payment , token ? ) { const chan = PaymentChannel . fromPayment ( payment ) ; await this . channelsDao . saveOrUpdate ( chan ) ; let _token = token || payment . token || uuid . v4 ( ) . replace ( /-/g , '' ) ; await this . paymentsDao . save ( _token , payment ) ; return payment ; } async acceptPayment ( payment ) { const isPaymentInTokens = ChannelInflator . isTokenContractDefined ( payment . tokenContract ) ; if ( isPaymentInTokens ) { LOG . info ( `Queueing payment of ${ payment . price . toString ( ) } token(s) to channel with ID ${ payment . channelId }.` ) ; } else { LOG . info ( `Queueing payment of ${ payment . price . toString ( ) } Wei to channel with ID ${ payment . channelId }.` ) ; } return this . mutex . synchronizeOn ( payment . channelId , async ( ) => { const channel = await this . findChannel ( payment ) ; if ( isPaymentInTokens ) { LOG . info ( `Adding ${ payment . price . toString ( ) } token(s) to channel with ID ${ channel . channelId . toString ( ) }.` ) ; } else { LOG . info ( `Adding ${ payment . price . toString ( ) } Wei to channel with ID ${ channel . channelId . toString ( ) }.` ) ; } const valid = await this . paymentManager . isValid ( payment , channel ) ; if ( valid ) { channel . spent = payment . value ; const token = this . web3 . sha3 ( JSON . stringify ( payment ) + ( new Date ( ) ) . toString ( ) ) . toString ( ) ; await Promise . all ( [ this . channelsDao . saveOrUpdate ( channel ) , this . tokensDao . save ( token , payment . channelId ) , this . paymentsDao . save ( token , payment ) ] ) ; return token ; } if ( this . machinomyOptions . closeOnInvalidPayment ) { LOG . info ( `Received invalid payment from ${ payment . sender }!` ) ; const existingChannel = await this . channelsDao . findBySenderReceiverChannelId ( payment . sender , payment . receiver , payment . channelId ) ; if ( existingChannel ) { LOG . info ( `Found existing channel with id ${ payment . channelId } between ${ payment . sender } and ${ payment . receiver }.` ) ; LOG . info ( 'Closing channel due to malfeasance.' ) ; await this . internalCloseChannel ( channel . channelId ) ; } } throw new PaymentNotValidError ( ) ; } ) ; } requireOpenChannel ( sender , receiver , amount , minDepositAmount ? , tokenContract ? ) { return this . mutex . synchronize ( async ( ) => { if ( ! minDepositAmount && this . machinomyOptions && this . machinomyOptions . minimumChannelAmount ) { minDepositAmount = new BigNumber . BigNumber ( this . machinomyOptions . minimumChannelAmount ) ; } let channel = await this . channelsDao . findUsable ( sender , receiver , amount ) ; return channel || this . internalOpenChannel ( sender , receiver , amount , minDepositAmount , undefined , tokenContract ) ; } ) ; } channels ( ) { return this . channelsDao . all ( ) ; } openChannels ( ) { return this . channelsDao . allOpen ( ) ; } settlingChannels ( ) { return this . channelsDao . allSettling ( ) ; } async channelById ( channelId ) { let channel = await this . channelsDao . firstById ( channelId ) ; let channelC = await this . channelContract . channelById ( channelId . toString ( ) ) ; if ( channel && channelC ) { channel . value = channelC [ 2 ] ; return channel ; } else { return this . handleUnknownChannel ( channelId ) ; } } verifyToken ( token ) { return this . tokensDao . isPresent ( token ) ; } param sender param receiver param remoteChannels async syncChannels ( sender , receiver , remoteChannels ) { const channels = await this . openChannels ( ) ; const recChannels = channels . filter ( chan => chan . receiver === receiver ) ; const promises = remoteChannels . map ( async ( remoteChan ) => { const localChan = recChannels . find ( chan => chan . channelId === remoteChan . channelId ) ; if ( localChan ) { if ( localChan . spent >= remoteChan . spent ) { return ; } } else { await this . nextPayment ( remoteChan . channelId , new BigNumber . BigNumber ( 0 ) , '' ) ; } const digest = await this . channelContract . paymentDigest ( remoteChan . channelId , remoteChan . spent ) ; const restored = recoverPersonalSignature ( { data : digest , sig : remoteChan . sign . toString ( ) } ) ; if ( restored !== sender ) { throw new InvalidChannelError ( 'signature' ) ; } const payment = await this . nextPayment ( remoteChan . channelId , remoteChan . spent , '' ) ; let chan = await this . findChannel ( payment ) ; chan . spent = remoteChan . spent ; await this . channelsDao . saveOrUpdate ( chan ) ; } ) ; await Promise . all ( promises ) ; } async lastPayment ( channelId ) { channelId = channelId . toString ( ) ; return this . paymentsDao . firstMaximum ( channelId ) ; } private async internalOpenChannel ( sender , receiver , amount , minDepositAmount = new BigNumber . BigNumber ( 0 ) , channelId ? , tokenContract ? ) { let depositAmount = amount . times ( 10 ) ; if ( minDepositAmount . greaterThan ( 0 ) && minDepositAmount . greaterThan ( depositAmount ) ) { depositAmount = minDepositAmount ; } this . emit ( 'willOpenChannel' , sender , receiver , depositAmount ) ; let settlementPeriod = this . machinomyOptions . settlementPeriod || ChannelManager . DEFAULT_SETTLEMENT_PERIOD ; let paymentChannel = await this . buildChannel ( sender , receiver , depositAmount , settlementPeriod , undefined , channelId , tokenContract ) ; await this . channelsDao . save ( paymentChannel ) ; this . emit ( 'didOpenChannel' , paymentChannel ) ; return paymentChannel ; } private async internalCloseChannel ( channelId ) { let channel = await this . channelById ( channelId ) || await this . handleUnknownChannel ( channelId ) ; if ( ! channel ) { throw new Error ( `Channel ${ channelId } not found.` ) ; } this . emit ( 'willCloseChannel' , channel ) ; let res ; if ( channel . sender === this . account ) { res = this . settle ( channel ) ; } else { res = this . claim ( channel ) ; } const txn = await res ; this . emit ( 'didCloseChannel' , channel ) ; return txn ; } private settle ( channel ) { return this . channelContract . getState ( channel . channelId ) . then ( async ( state ) => { if ( state === 2 ) { throw new Error ( `Channel ${ channel . channelId . toString ( ) } is already settled.` ) ; } switch ( state ) { case 0 : { const block = await pify ( ( cb ) => { this . web3 . eth . getBlock ( 'latest' , cb ) ; } ) ; const settlingUntil = new BigNumber . BigNumber ( block . number ) . plus ( channel . settlementPeriod ) ; const res = await this . channelContract . startSettle ( this . account , channel . channelId ) ; await this . channelsDao . updateState ( channel . channelId , 1 ) ; await this . channelsDao . updateSettlingUntil ( channel . channelId , settlingUntil ) ; return res ; } case 1 : return this . channelContract . finishSettle ( this . account , channel . channelId ) . then ( ( res ) => this . channelsDao . updateState ( channel . channelId , 2 ) . then ( ( ) => res ) ) ; default : throw new Error ( `Unknown state: ${ state }` ) ; } } ) ; } private async claim ( channel ) { let payment = await this . lastPayment ( channel . channelId ) ; if ( payment ) { let result = await this . channelContract . claim ( channel . receiver , channel . channelId , payment . value , payment . signature ) ; await this . channelsDao . updateState ( channel . channelId , 2 ) ; return result ; } else { throw new Error ( 'Can not claim unknown channnel' ) ; } } private async buildChannel ( sender , receiver , price , settlementPeriod , settlingUntil , channelId ? , tokenContract ? ) { const res = await this . channelContract . open ( sender , receiver , price , settlementPeriod , channelId , tokenContract ) ; const _channelId = res . logs [ 0 ] . args . channelId ; return new PaymentChannel ( sender , receiver , _channelId , price , new BigNumber . BigNumber ( 0 ) , 0 , tokenContract , settlementPeriod , settlingUntil ) ; } private async handleUnknownChannel ( channelId ) { channelId = channelId . toString ( ) ; const channel = await this . channelContract . channelById ( channelId ) ; if ( ! channel ) return null ; const sender = channel [ 0 ] ; const receiver = channel [ 1 ] ; const value = channel [ 2 ] ; const settlingUntil = channel [ 4 ] ; if ( sender !== this . account && receiver !== this . account ) { return null ; } const chan = new PaymentChannel ( sender , receiver , channelId , value , new BigNumber . BigNumber ( 0 ) , settlingUntil . eq ( 0 ) ? 0 : 1 , '' , undefined , settlingUntil ) ; await this . channelsDao . save ( chan ) ; return chan ; } private async findChannel ( payment ) { let chan = await this . channelsDao . findBySenderReceiverChannelId ( payment . sender , payment . receiver , payment . channelId ) ; if ( chan ) { return chan ; } chan = PaymentChannel . fromPayment ( payment ) ; chan . spent = new BigNumber . BigNumber ( 0 ) ; return chan ; } }