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); }); }); });