11import { EventEmitter } from 'events'
2- import * as assert from 'assert'
32import createLogger from 'ilp-logger'
43import { DataAndMoneyStream } from './stream'
54import * as IlpPacket from 'ilp-packet'
@@ -29,15 +28,12 @@ import { Reader } from 'oer-utils'
2928import { CongestionController } from './util/congestion'
3029import { Plugin } from './util/plugin-interface'
3130import {
32- LongValue ,
33- longFromValue ,
3431 maxLong ,
3532 minLong ,
3633 minLongs ,
3734 countDigits ,
3835 checkedAdd ,
3936 checkedSubtract ,
40- checkedMultiply ,
4137 multiplyDivideFloor
4238} from './util/long'
4339import * as Long from 'long'
@@ -55,6 +51,8 @@ const DEFAULT_MINIMUM_EXCHANGE_RATE_PRECISION = 3
5551const TEST_PACKET_MAX_ATTEMPTS = 20
5652
5753export interface ConnectionOpts {
54+ /** Token in the ILP address uniquely identifying this connection */
55+ connectionId ?: string ,
5856 /** Ledger plugin (V2) */
5957 plugin : Plugin ,
6058 /** ILP Address of the remote entity */
@@ -93,6 +91,17 @@ export interface ConnectionOpts {
9391 * When omitted, use a timeout of 30 seconds.
9492 */
9593 getExpiry ?: ( destination : string ) => Date ,
94+ /**
95+ * Callback for the consumer to perform accounting and choose to fulfill an incoming ILP Prepare,
96+ * given the amount received, a unique identifier for the packet, and `connectionTag`.
97+ *
98+ * If the returned Promise is resolved, the ILP Prepare will be fulfilled; if it is rejected,
99+ * the ILP Prepare will be rejected. The ILP Fulfill will be immediately sent back after
100+ * the Promise is resolved.
101+ *
102+ * Note: a misbehaving sender can trigger duplicate packetIds, which should be ignored and rejected.
103+ */
104+ shouldFulfill ?: ( packetAmount : Long , packetId : Buffer , connectionTag ?: string ) => Promise < void > ,
96105}
97106
98107export interface BuildConnectionOpts extends ConnectionOpts {
@@ -181,15 +190,21 @@ export class Connection extends EventEmitter {
181190
182191 // TODO use longs for byte offsets
183192 protected remoteMaxOffset : number
193+ protected _incomingHold : Long
184194 protected _totalReceived : Long
185195 protected _totalSent : Long
186196 protected _totalDelivered : Long
187197 protected _lastPacketExchangeRate : Rational
188198 protected getExpiry : ( destination : string ) => Date
199+ protected shouldFulfill ?: ( packetAmount : Long , packetId : Buffer , connectionTag ?: string ) => Promise < void >
189200
190201 constructor ( opts : NewConnectionOpts ) {
191202 super ( )
192- this . connectionId = uuid ( )
203+
204+ // Use the same connectionId for logging on both client & server
205+ const lastAddressSegment = opts . destinationAccount ? opts . destinationAccount . split ( '.' ) . slice ( - 1 ) [ 0 ] : undefined
206+ this . connectionId = ( opts . connectionId || lastAddressSegment || uuid ( ) ) . replace ( / [ - _ ] / g, '' ) . slice ( 0 , 8 )
207+
193208 this . plugin = opts . plugin
194209 this . _sourceAccount = opts . sourceAccount
195210 this . _sourceAssetCode = opts . assetCode
@@ -213,6 +228,7 @@ export class Connection extends EventEmitter {
213228 ? undefined
214229 : Rational . fromNumber ( opts . exchangeRate , true )
215230 this . getExpiry = opts . getExpiry || defaultGetExpiry
231+ this . shouldFulfill = opts . shouldFulfill
216232 this . idleTimeout = opts . idleTimeout || DEFAULT_IDLE_TIMEOUT
217233 this . lastActive = new Date ( )
218234
@@ -239,6 +255,7 @@ export class Connection extends EventEmitter {
239255
240256 this . remoteMaxOffset = this . maxBufferedData
241257
258+ this . _incomingHold = Long . UZERO
242259 this . _totalReceived = Long . UZERO
243260 this . _totalSent = Long . UZERO
244261 this . _totalDelivered = Long . UZERO
@@ -476,7 +493,8 @@ export class Connection extends EventEmitter {
476493
477494 /**
478495 * (Internal) Handle incoming ILP Prepare packets.
479- * This will automatically fulfill all valid and expected Prepare packets.
496+ * This will automatically fulfill all valid and expected Prepare packets, or
497+ * defer to custom application logic using the `shouldFulfill` callback, if provided.
480498 * It passes the incoming money and/or data to the relevant streams.
481499 * @private
482500 */
@@ -628,9 +646,16 @@ export class Connection extends EventEmitter {
628646 }
629647 }
630648
631- // Add incoming amounts to each stream
632- for ( let { stream, amount } of amountsToReceive ) {
633- stream . _addToIncoming ( amount )
649+ this . addIncomingHold ( incomingAmount )
650+
651+ // Allow consumer to choose to fulfill each packet and/or perform other logic before fulfilling
652+ if ( this . shouldFulfill && incomingAmount . greaterThan ( 0 ) ) {
653+ const packetId = await cryptoHelper . generateIncomingPacketId ( this . sharedSecret , requestPacket . sequence )
654+ await this . shouldFulfill ( incomingAmount , packetId , this . connectionTag ) . catch ( async err => {
655+ this . removeIncomingHold ( incomingAmount )
656+ this . log . debug ( 'application declined to fulfill packet %s:' , requestPacket . sequence , err )
657+ await throwFinalApplicationError ( )
658+ } )
634659 }
635660
636661 // Tell peer about closed streams and how much each stream can receive
@@ -655,12 +680,18 @@ export class Connection extends EventEmitter {
655680 }
656681 }
657682
683+ // Add incoming amounts to each stream
684+ for ( let { stream, amount } of amountsToReceive ) {
685+ stream . _addToIncoming ( amount )
686+ }
687+
658688 // TODO make sure the queued frames aren't too big
659689 responseFrames = responseFrames . concat ( this . queuedFrames )
660690 this . queuedFrames = [ ]
661691
662692 // Return fulfillment and response packet
663693 const responsePacket = new Packet ( requestPacket . sequence , IlpPacketType . Fulfill , incomingAmount , responseFrames )
694+ this . removeIncomingHold ( incomingAmount )
664695 this . addTotalReceived ( incomingAmount )
665696 this . log . trace ( 'fulfilling prepare with fulfillment: %h and response packet: %j' , fulfillment , responsePacket )
666697 return {
@@ -1588,19 +1619,33 @@ export class Connection extends EventEmitter {
15881619
15891620 private bumpIdle ( ) : void { this . lastActive = new Date ( ) }
15901621
1591- private addTotalReceived ( value : Long ) : void {
1592- const result = checkedAdd ( this . _totalReceived , value )
1622+ private addIncomingHold ( value : Long ) : void {
1623+ let result = checkedAdd ( this . _totalReceived , this . _incomingHold )
1624+ result = checkedAdd ( result . sum , value )
1625+
15931626 if ( result . overflow ) {
15941627 const err = new IlpPacket . Errors . BadRequestError ( 'Total received exceeded MaxUint64' )
15951628 err [ 'ilpErrorMessage' ] = err . message
15961629 /* tslint:disable-next-line:no-floating-promises */
15971630 this . destroy ( err )
15981631 throw err
15991632 } else {
1600- this . _totalReceived = result . sum
1633+ this . _incomingHold = result . sum
16011634 }
16021635 }
16031636
1637+ private removeIncomingHold ( value : Long ) : void {
1638+ // As long as this is called after `addIncomingHold` for the same amount,
1639+ // this should never underflow
1640+ this . _incomingHold = checkedSubtract ( this . _incomingHold , value ) . difference
1641+ }
1642+
1643+ private addTotalReceived ( value : Long ) : void {
1644+ // As long as this is called after `addIncomingHold` for the same amount,
1645+ // this should never overflow
1646+ this . _totalReceived = checkedAdd ( this . _totalReceived , value ) . sum
1647+ }
1648+
16041649 private addTotalSent ( value : Long ) : void {
16051650 const result = checkedAdd ( this . _totalSent , value )
16061651 if ( result . overflow ) {
0 commit comments