import * as _ from 'lodash' ; import { checkThat , isSimpleAllocation } from '../utils' ; import { BN } from '../bignumber' ; import { calculateChannelId , hashState , signState } from '../state-utils' ; import { Address , SignatureEntry , SignedState , State , Uint256 } from '../types' ; import { BaseObjective , BaseObjectiveEvent } from './objective' ; export const MAX_WAITING_TIME = 5000 ; export type OpenChannelEvent = BaseObjectiveEvent & ( { type : 'Crank' ; } | { type : 'StatesReceived' ; states : SignedState [ ] ; } | { type : 'FundingUpdated' ; amount : Uint256 ; finalized : boolean ; } | { type : 'DepositSubmitted' ; tx : string ; attempt : number ; submittedAt : number ; } | { type : 'Approval' ; } ) ; export type SignedStateHash = { hash : string ; signatures : SignatureEntry [ ] ; } ; export enum WaitingFor { approval = 'DirectFunder.approval' , theirPreFundSetup = 'DirectFunder.theirPreFundSetup' , safeToDeposit = 'DirectFunder.safeToDeposit' , channelFunded = 'DirectFunder.channelFunded' , theirPostFundState = 'DirectFunder.theirPostFundSetup' } const enum Steps { preFundSetup = 'preFundSetup' , postFundSetup = 'postFundSetup' } export type OpenChannelObjective = BaseObjective & { status : WaitingFor | 'success' | 'error' ; approved : boolean ; channelId : string ; openingState : State ; myIndex : number ; preFundSetup : SignedStateHash ; funding : { amount : Uint256 ; finalized : boolean ; } ; fundingRequest : { tx : string ; attempts : number ; submittedAt : number ; } | undefined ; postFundSetup : SignedStateHash ; } ; export function initialize ( openingState , myIndex , approved = false ) { if ( openingState . turnNum !== 0 ) { throw new Error ( `Unexpected state due to turnNum ${ openingState . turnNum }` ) ; } const allowedIndices = _ . range ( 0 , openingState . participants . length ) ; if ( ! allowedIndices . includes ( myIndex ) ) { throw new Error ( `Unexpected index due to index ${ myIndex }` ) ; } utils . fundingMilestone ( openingState , openingState . participants [ myIndex ] . destination ) ; const signatures = 'signatures' in openingState ? mergeSignatures ( [ ] , openingState . signatures ) : [ ] ; return { type : 'OpenChannel' , approved , channelId : calculateChannelId ( openingState ) , myIndex , openingState : _ . omit ( openingState , 'signatures' ) , status : approved ? WaitingFor . theirPreFundSetup : WaitingFor . approval , preFundSetup : { hash : hashState ( setupState ( openingState , Steps . preFundSetup ) ) , signatures } , funding : { amount : BN . from ( 0 ) , finalized : true } , fundingRequest : undefined , postFundSetup : { hash : hashState ( setupState ( openingState , Steps . postFundSetup ) ) , signatures : [ ] } } ; } export type Action = { type : 'sendStates' ; states : SignedState [ ] ; } | { type : 'deposit' ; amount : Uint256 ; } | { type : 'handleError' ; error : Error ; } ; export type OpenChannelResult = { objective : OpenChannelObjective ; actions : Action [ ] ; } ; param objective param event param myPrivateKey returns export function openChannelCranker ( currentObjectiveState , event , myPrivateKey ) { const objective = _ . cloneDeep ( currentObjectiveState ) ; const actions = [ ] ; const { participants } = objective . openingState ; const me = participants [ objective . myIndex ] ; switch ( event . type ) { case 'Crank' : break ; case 'Approval' : objective . approved = true ; break ; case 'DepositSubmitted' : objective . fundingRequest = { tx : event . tx , submittedAt : event . submittedAt , attempts : event . attempt } ; break ; case 'FundingUpdated' : objective . funding . amount = event . amount ; objective . funding . finalized = event . finalized ; break ; case 'StatesReceived' : { const { states : signedStates } = event ; if ( signedStates && signedStates [ 0 ] ) { const signedState = signedStates [ 0 ] ; const hash = hashState ( signedState ) ; const { signatures } = signedState ; for ( const signature of signatures ) { if ( ! participants . find ( p => p . signingAddress === signature . signer ) ) { objective . status = 'error' ; const error = new DirectFunderError ( { message : 'NonParticipantSignature' , signature } ) ; actions . push ( { type : 'handleError' , error } ) ; return { objective , actions } ; } } if ( hash === objective . preFundSetup . hash ) { objective . preFundSetup . signatures = mergeSignatures ( objective . preFundSetup . signatures , signatures ) ; } else if ( hash === objective . postFundSetup . hash ) { objective . postFundSetup . signatures = mergeSignatures ( objective . postFundSetup . signatures , signatures ) ; } else { objective . status = 'error' ; const error = new DirectFunderError ( { message : 'ReceivedUnexpectedState' , received : hash , expected : [ objective . preFundSetup . hash , objective . postFundSetup . hash ] } ) ; actions . push ( { type : 'handleError' , error } ) ; return { objective , actions } ; } } break ; } default : return handleUnexpectedEvent ( event , objective ) ; } if ( ! objective . approved ) { objective . status = WaitingFor . approval ; return { objective , actions } ; } if ( ! signedbyMe ( objective , Steps . preFundSetup , me . signingAddress ) ) { signStateAction ( Steps . preFundSetup , myPrivateKey , objective , actions ) ; } if ( ! completed ( objective , Steps . preFundSetup ) ) { objective . status = WaitingFor . theirPreFundSetup ; return { objective , actions } ; } const { funding } = objective ; const { targetBefore , targetAfter , targetTotal } = utils . fundingMilestone ( objective . openingState , me . destination ) ; if ( BN . gte ( funding . amount , targetTotal ) && funding . finalized ) { } else if ( BN . lt ( funding . amount , targetBefore ) ) { objective . status = WaitingFor . safeToDeposit ; return { objective , actions } ; } else if ( BN . gte ( funding . amount , targetAfter ) ) { objective . status = WaitingFor . channelFunded ; return { objective , actions } ; } else if ( objective . fundingRequest ) { if ( event . now && event . now >= objective . fundingRequest . submittedAt + MAX_WAITING_TIME ) { objective . status = 'error' ; const error = new DirectFunderError ( { message : 'TimedOutWhileFunding' , now : event . now , submittedAt : objective . fundingRequest . submittedAt } ) ; actions . push ( { type : 'handleError' , error } ) ; return { objective , actions } ; } else { objective . status = WaitingFor . channelFunded ; return { objective , actions } ; } } else { const amount = BN . sub ( targetAfter , funding . amount ) ; actions . push ( { type : 'deposit' , amount } ) ; objective . status = WaitingFor . channelFunded ; return { objective , actions } ; } if ( ! signedbyMe ( objective , Steps . postFundSetup , me . signingAddress ) ) { signStateAction ( Steps . postFundSetup , myPrivateKey , objective , actions ) ; } if ( ! completed ( objective , Steps . postFundSetup ) ) { objective . status = WaitingFor . theirPostFundState ; return { objective , actions } ; } else { objective . status = 'success' ; return { objective , actions } ; } } function signedbyMe ( objective , step , me ) { return objective [ step ] . signatures . map ( e => e . signer ) . includes ( me ) ; } function completed ( objective , step ) { const { openingState : firstState } = objective ; return objective [ step ] . signatures . length === firstState . participants . length ; } function setupState ( openingState , step ) { const turnNum = step === Steps . preFundSetup ? 0 : 2 * openingState . participants . length - 1 ; return { ... openingState , turnNum } ; } function signStateAction ( step , myPrivateKey , objective , actions ) { const { openingState , myIndex } = objective ; const me = openingState . participants [ myIndex ] ; const state = setupState ( openingState , step ) ; const signature = signState ( state , myPrivateKey ) ; const entry = { signature , signer : me . signingAddress } ; objective [ step ] . signatures = mergeSignatures ( objective [ step ] . signatures , [ entry ] ) ; const signedState = { ... state , signatures : [ entry ] } ; const existingAction = actions . find ( a => a . type === 'sendStates' ) ; if ( existingAction ) { ( existingAction as any ) . states . push ( signedState ) ; } else { actions . push ( { type : 'sendStates' , states : [ { ... state , signatures : [ entry ] } ] } ) ; } return actions ; } function mergeSignatures ( left , right ) { const unsorted = _ . uniqBy ( _ . concat ( left , right ) , entry => entry . signer ) ; return _ . sortBy ( unsorted , entry => entry . signer ) ; } type FundingMilestone = { targetBefore : Uint256 ; targetAfter : Uint256 ; targetTotal : Uint256 ; } ; export const utils = { fundingMilestone ( state , destination ) { const { allocationItems } = checkThat ( state . outcome , isSimpleAllocation ) ; const myAllocationItem = _ . find ( allocationItems , ai => ai . destination === destination ) ; if ( ! myAllocationItem ) { throw new Error ( 'unexpected outcome' ) ; } const allocationsBefore = _ . takeWhile ( allocationItems , a => a . destination !== destination ) ; const targetBefore = allocationsBefore . map ( a => a . amount ) . reduce ( BN . add , BN . from ( 0 ) ) ; const targetAfter = BN . add ( targetBefore , myAllocationItem . amount ) ; const targetTotal = allocationItems . map ( a => a . amount ) . reduce ( BN . add , BN . from ( 0 ) ) ; return { targetBefore , targetAfter , targetTotal } ; } } ; export type ErrorModes = { message : 'NonParticipantSignature' ; signature : SignatureEntry ; } | { message : 'ReceivedUnexpectedState' ; received : string ; expected : [ string , string ] ; } | { message : 'TimedOutWhileFunding' ; now : number ; submittedAt : number ; } | { message : 'UnexpectedEvent' ; event : any ; } ; class DirectFunderError extends Error { constructor ( public data ) { super ( data . message ) ; } } param event param objective returns function handleUnexpectedEvent ( event , objective ) { const error = new DirectFunderError ( { event , message : 'UnexpectedEvent' } ) ; objective . status = 'error' ; return { objective , actions : [ { type : 'handleError' , error } ] } ; }