From cb701b670affefc5326a858db009bfbbec34599f Mon Sep 17 00:00:00 2001 From: Daniel McNally Date: Thu, 10 Sep 2020 03:14:37 -0400 Subject: [PATCH] feat(connext): request collateral for order amount This adds a check when a new order is receiving tokens via connext that ensures we have sufficient collateral from the connext node to fulfill the order. If not, we fail the order request and request the additional collateral necessary. This involves the following changes: 1. The Connext client now tracks the inbound node collateral for each currency and refreshes this value every time it queries for channel balances and every time a collateral request completes. 2. Before placing a new order that is receiving Connext tokens we check the inbound amount against the available collateral for that currency. 3. If the collateral is insufficient, the order will be rejected and a collateral request will be performed for the missing capacity. 4. Any other collateral checks while we're awaiting a response for a collateral request will not perform an additional collateral request so as to prevent duplicate calls. 5. Traders may repeat their order request and it will be accepted once sufficient collateral to complete the trade is acquired. Closes #1845. --- lib/connextclient/ConnextClient.ts | 39 ++++++++++++++++++++++++------ lib/connextclient/errors.ts | 5 ++++ lib/connextclient/types.ts | 1 + lib/lndclient/LndClient.ts | 4 +-- lib/orderbook/OrderBook.ts | 13 ++++++---- lib/swaps/SwapClient.ts | 6 ++++- test/jest/LndClient.spec.ts | 2 +- 7 files changed, 54 insertions(+), 16 deletions(-) diff --git a/lib/connextclient/ConnextClient.ts b/lib/connextclient/ConnextClient.ts index 0d1e4de6d..565b963ac 100644 --- a/lib/connextclient/ConnextClient.ts +++ b/lib/connextclient/ConnextClient.ts @@ -40,8 +40,6 @@ import { Observable, fromEvent, from, combineLatest, defer, timer } from 'rxjs'; import { take, pluck, timeout, filter, mergeMap, catchError, mergeMapTo } from 'rxjs/operators'; import { sha256 } from '@ethersproject/solidity'; -const MAX_AMOUNT = Number.MAX_SAFE_INTEGER; - interface ConnextClient { on(event: 'preimage', listener: (preimageRequest: ProvidePreimageEvent) => void): void; on(event: 'transferReceived', listener: (transferReceivedRequest: TransferReceivedEvent) => void): void; @@ -114,7 +112,10 @@ class ConnextClient extends SwapClient { private seed: string | undefined; /** A map of currencies to promises representing balance requests. */ private getBalancePromises = new Map>(); + /** A map of currencies to promises representing collateral requests. */ + private requestCollateralPromises = new Map>(); private _totalOutboundAmount = new Map(); + private _maxChannelInboundAmount = new Map(); /** * Creates a connext client. @@ -259,9 +260,27 @@ class ConnextClient extends SwapClient { return this._totalOutboundAmount.get(currency) || 0; } - public maxChannelInboundAmount = (_currency: string): number => { - // assume MAX_AMOUNT since Connext will re-collaterize accordingly - return MAX_AMOUNT; + public checkInboundCapacity = (inboundAmount: number, currency: string) => { + const inboundCapacity = this._maxChannelInboundAmount.get(currency) || 0; + if (inboundCapacity < inboundAmount) { + // we do not have enough inbound capacity to receive the specified inbound amount so we must request collateral + this.logger.debug(`collateral of ${inboundCapacity} for ${currency} is insufficient for order amount ${inboundAmount}`); + const capacityShortage = inboundAmount - inboundCapacity; + const capacityShortageUnits = this.unitConverter.amountToUnits({ currency, amount: capacityShortage }); + + // first check whether we already have a pending collateral request for this currency + // if not start a new request, and when it completes call channelBalance to refresh our inbound capacity + const requestCollateralPromise = this.requestCollateralPromises.get(currency) ?? this.sendRequest('/request-collateral', 'POST', { + assetId: this.tokenAddresses.get(currency), + amount: capacityShortageUnits, + }).then(() => { + this.logger.debug(`completed collateral request of ${capacityShortageUnits} ${currency} units`); + return this.channelBalance(currency); + }).catch(this.logger.error); + this.requestCollateralPromises.set(currency, requestCollateralPromise); + + throw errors.INSUFFICIENT_COLLATERAL; + } } protected updateCapacity = async () => { @@ -585,14 +604,20 @@ class ConnextClient extends SwapClient { return { balance: 0, pendingOpenBalance: 0, inactiveBalance: 0 }; } - const { freeBalanceOffChain } = await this.getBalance(currency); + const { freeBalanceOffChain, nodeFreeBalanceOffChain } = await this.getBalance(currency); const freeBalanceAmount = this.unitConverter.unitsToAmount({ currency, units: Number(freeBalanceOffChain), }); + const nodeFreeBalanceAmount = this.unitConverter.unitsToAmount({ + currency, + units: Number(nodeFreeBalanceOffChain), + }); this._totalOutboundAmount.set(currency, freeBalanceAmount); + this._maxChannelInboundAmount.set(currency, nodeFreeBalanceAmount); + this.logger.trace(`new inbound capacity (collateral) for ${currency} of ${nodeFreeBalanceAmount}`); return { balance: freeBalanceAmount, inactiveBalance: 0, @@ -604,7 +629,7 @@ class ConnextClient extends SwapClient { await this.channelBalance(currency); // refreshes the max outbound balance return { maxSell: this.maxChannelOutboundAmount(currency), - maxBuy: this.maxChannelInboundAmount(currency), + maxBuy: this._maxChannelInboundAmount.get(currency) ?? 0, }; } diff --git a/lib/connextclient/errors.ts b/lib/connextclient/errors.ts index ad3c152ab..ed22a92a4 100644 --- a/lib/connextclient/errors.ts +++ b/lib/connextclient/errors.ts @@ -17,6 +17,7 @@ const errorCodes = { CURRENCY_MISSING: codesPrefix.concat('.14'), EXPIRY_MISSING: codesPrefix.concat('.15'), MISSING_SEED: codesPrefix.concat('.16'), + INSUFFICIENT_COLLATERAL: codesPrefix.concat('.17'), }; const errors = { @@ -76,6 +77,10 @@ const errors = { message: 'seed is missing', code: errorCodes.MISSING_SEED, }, + INSUFFICIENT_COLLATERAL: { + message: 'channel collateralization in progress, please try again in ~1 minute', + code: errorCodes.INSUFFICIENT_COLLATERAL, + }, }; export { errorCodes }; diff --git a/lib/connextclient/types.ts b/lib/connextclient/types.ts index f1a7d38ee..e8aee29e5 100644 --- a/lib/connextclient/types.ts +++ b/lib/connextclient/types.ts @@ -64,6 +64,7 @@ export type ConnextConfigResponse = { */ export type ConnextBalanceResponse = { freeBalanceOffChain: string; + nodeFreeBalanceOffChain: string; freeBalanceOnChain: string; }; diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index 443ca5b15..78aee9891 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -197,8 +197,8 @@ class LndClient extends SwapClient { return this._maxChannelOutboundAmount; } - public maxChannelInboundAmount = () => { - return this._maxChannelInboundAmount; + public checkInboundCapacity = (_inboundAmount: number) => { + return; // we do not currently check inbound capacities for lnd } /** Lnd specific procedure to mark the client as locked. */ diff --git a/lib/orderbook/OrderBook.ts b/lib/orderbook/OrderBook.ts index d22e343f3..08e01c1cb 100644 --- a/lib/orderbook/OrderBook.ts +++ b/lib/orderbook/OrderBook.ts @@ -459,13 +459,13 @@ class OrderBook extends EventEmitter { }; } - const { outboundCurrency, inboundCurrency, outboundAmount } = - Swaps.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); - const outboundSwapClient = this.swaps.swapClientManager.get(outboundCurrency); - const inboundSwapClient = this.swaps.swapClientManager.get(inboundCurrency); - if (!this.nobalancechecks) { + const { outboundCurrency, inboundCurrency, outboundAmount, inboundAmount } = + Swaps.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); + // check if clients exists + const outboundSwapClient = this.swaps.swapClientManager.get(outboundCurrency); + const inboundSwapClient = this.swaps.swapClientManager.get(inboundCurrency); if (!outboundSwapClient) { throw swapsErrors.SWAP_CLIENT_NOT_FOUND(outboundCurrency); } @@ -478,6 +478,9 @@ class OrderBook extends EventEmitter { if (outboundAmount > totalOutboundAmount) { throw errors.INSUFFICIENT_OUTBOUND_BALANCE(outboundCurrency, outboundAmount, totalOutboundAmount); } + + // check if sufficient inbound channel capacity exists + inboundSwapClient.checkInboundCapacity(inboundAmount, inboundCurrency); } let replacedOrderIdentifier: OrderIdentifier | undefined; diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index 58009ec66..2ae4783aa 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -133,7 +133,11 @@ abstract class SwapClient extends EventEmitter { public abstract totalOutboundAmount(currency?: string): number; public abstract maxChannelOutboundAmount(currency?: string): number; - public abstract maxChannelInboundAmount(currency?: string): number; + /** + * Checks whether there is sufficient inbound capacity to receive the specified amount + * and throws an error if there isn't, otherwise does nothing. + */ + public abstract checkInboundCapacity(inboundAmount: number, currency?: string): void; protected abstract updateCapacity(): Promise; public verifyConnectionWithTimeout = () => { diff --git a/test/jest/LndClient.spec.ts b/test/jest/LndClient.spec.ts index 79902fafa..49c77429b 100644 --- a/test/jest/LndClient.spec.ts +++ b/test/jest/LndClient.spec.ts @@ -264,7 +264,7 @@ describe('LndClient', () => { expect(lnd['listChannels']).toHaveBeenCalledTimes(1); expect(lnd.maxChannelOutboundAmount()).toEqual(98); - expect(lnd.maxChannelInboundAmount()).toEqual(295); + expect(lnd['_maxChannelInboundAmount']).toEqual(295); }); }); });