From 9721589dd1d01b6b0c0e738b94dcbc3be483ef68 Mon Sep 17 00:00:00 2001 From: Daniel McNally Date: Wed, 21 Oct 2020 06:17:47 -0400 Subject: [PATCH] feat: reserved capacity checks on PlaceOrder This rejects orders that would put our total reserved balance over our total capacity for either the outbound or inbound currency. The sum of the inbound & outbound amounts for a newly placed order are added to the amounts reserved by open orders, and if either of these amounts exceed the corresponding capacity then the request to place the order is rejected. An exception to this are inbound limits for Connext currencies, since we have the ability to dynamically request additional inbound collateral via our "lazy collateral" approach. It is still possible for market orders to cause our open orders to exceed our capacity. This is a difficult problem to avoid entirely, as the price that market orders will execute at is unknown until the execution is complete. Even if we simulate the matching routine, we won't know which matches will succeed until we attempt a swap. Instead, we generously assume that market orders will execute at the best quoted price for purposes of these capacity checks. For users that simultaneously place limit orders and market orders for the same currencies, it should be made clear that market orders may use up their available balance needed for their limit orders to succeed. Closes #1947. --- lib/connextclient/ConnextClient.ts | 8 +- lib/grpc/getGrpcError.ts | 3 +- lib/lndclient/LndClient.ts | 14 +-- lib/orderbook/OrderBook.ts | 28 +---- lib/orderbook/errors.ts | 5 - lib/swaps/SwapClient.ts | 6 - lib/swaps/SwapClientManager.ts | 51 +++++++- lib/swaps/Swaps.ts | 40 +----- lib/swaps/errors.ts | 10 ++ lib/utils/UnitConverter.ts | 47 ++++++-- test/jest/Orderbook.spec.ts | 114 ++---------------- test/jest/SwapClientManager.spec.ts | 114 +++++++++++++++--- test/jest/UnitConverter.spec.ts | 62 +++++++++- .../jest/__snapshots__/Orderbook.spec.ts.snap | 8 -- .../SwapClientManager.spec.ts.snap | 7 -- test/unit/Swaps.spec.ts | 57 +-------- 16 files changed, 272 insertions(+), 302 deletions(-) delete mode 100644 test/jest/__snapshots__/Orderbook.spec.ts.snap diff --git a/lib/connextclient/ConnextClient.ts b/lib/connextclient/ConnextClient.ts index b92a1533a..009efc3f5 100644 --- a/lib/connextclient/ConnextClient.ts +++ b/lib/connextclient/ConnextClient.ts @@ -259,10 +259,6 @@ class ConnextClient extends SwapClient { }); } - public totalOutboundAmount = (currency: string): number => { - return this.outboundAmounts.get(currency) || 0; - } - /** * Checks whether we have a pending collateral request for the currency and, * if one doesn't exist, starts a new request for the specified amount. Then @@ -285,6 +281,10 @@ class ConnextClient extends SwapClient { this.requestCollateralPromises.set(currency, requestCollateralPromise); } + /** + * Checks whether there is sufficient inbound capacity to receive the specified amount + * and throws an error if there isn't, otherwise does nothing. + */ public checkInboundCapacity = (inboundAmount: number, currency: string) => { const inboundCapacity = this.inboundAmounts.get(currency) || 0; if (inboundCapacity < inboundAmount) { diff --git a/lib/grpc/getGrpcError.ts b/lib/grpc/getGrpcError.ts index 1d50636c3..f20f5c7e7 100644 --- a/lib/grpc/getGrpcError.ts +++ b/lib/grpc/getGrpcError.ts @@ -50,7 +50,8 @@ const getGrpcError = (err: any) => { case orderErrorCodes.CURRENCY_CANNOT_BE_REMOVED: case orderErrorCodes.MARKET_ORDERS_NOT_ALLOWED: case serviceErrorCodes.NOMATCHING_MODE_IS_REQUIRED: - case orderErrorCodes.INSUFFICIENT_OUTBOUND_BALANCE: + case swapErrorCodes.INSUFFICIENT_OUTBOUND_CAPACITY: + case swapErrorCodes.INSUFFICIENT_INBOUND_CAPACITY: case orderErrorCodes.QUANTITY_ON_HOLD: case swapErrorCodes.SWAP_CLIENT_NOT_FOUND: case swapErrorCodes.SWAP_CLIENT_MISCONFIGURED: diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index 9c55d7bd2..9e50391ac 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -62,7 +62,7 @@ class LndClient extends SwapClient { private channelBackupSubscription?: ClientReadableStream; private invoiceSubscriptions = new Map>(); private initRetryTimeout?: NodeJS.Timeout; - private _totalOutboundAmount = 0; + private totalOutboundAmount = 0; private totalInboundAmount = 0; private maxChannelOutboundAmount = 0; private maxChannelInboundAmount = 0; @@ -192,14 +192,6 @@ class LndClient extends SwapClient { return this.chainIdentifier; } - public totalOutboundAmount = () => { - return this._totalOutboundAmount; - } - - public checkInboundCapacity = (_inboundAmount: number) => { - return; // we do not currently check inbound capacities for lnd - } - public setReservedInboundAmount = (_reservedInboundAmount: number) => { return; // not currently used for lnd } @@ -742,8 +734,8 @@ class LndClient extends SwapClient { this.logger.debug(`new channel inbound capacity: ${maxInbound}`); } - if (this._totalOutboundAmount !== totalOutboundAmount) { - this._totalOutboundAmount = totalOutboundAmount; + if (this.totalOutboundAmount !== totalOutboundAmount) { + this.totalOutboundAmount = totalOutboundAmount; this.logger.debug(`new channel total outbound capacity: ${totalOutboundAmount}`); } diff --git a/lib/orderbook/OrderBook.ts b/lib/orderbook/OrderBook.ts index 008456973..af542ace5 100644 --- a/lib/orderbook/OrderBook.ts +++ b/lib/orderbook/OrderBook.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import { EventEmitter } from 'events'; +import { UnitConverter } from '../utils/UnitConverter'; import uuidv1 from 'uuid/v1'; import { SwapClientType, SwapFailureReason, SwapPhase, SwapRole } from '../constants/enums'; import { Models } from '../db/DB'; @@ -8,7 +9,6 @@ import Logger from '../Logger'; import { SwapFailedPacket, SwapRequestPacket } from '../p2p/packets'; import Peer from '../p2p/Peer'; import Pool from '../p2p/Pool'; -import swapsErrors from '../swaps/errors'; import Swaps from '../swaps/Swaps'; import { SwapDeal, SwapFailure, SwapSuccess } from '../swaps/types'; import { pubKeyToAlias } from '../utils/aliasUtils'; @@ -121,7 +121,7 @@ class OrderBook extends EventEmitter { const onOrderRemoved = (order: OwnOrder) => { const { inboundCurrency, outboundCurrency, inboundAmount, outboundAmount } = - Swaps.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); + UnitConverter.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); this.swaps.swapClientManager.subtractInboundReservedAmount(inboundCurrency, inboundAmount); this.swaps.swapClientManager.subtractOutboundReservedAmount(outboundCurrency, outboundAmount); }; @@ -130,7 +130,7 @@ class OrderBook extends EventEmitter { this.on('ownOrder.added', (order) => { const { inboundCurrency, outboundCurrency, inboundAmount, outboundAmount } = - Swaps.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); + UnitConverter.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); this.swaps.swapClientManager.addInboundReservedAmount(inboundCurrency, inboundAmount); this.swaps.swapClientManager.addOutboundReservedAmount(outboundCurrency, outboundAmount); }); @@ -487,27 +487,7 @@ class OrderBook extends EventEmitter { (order.isBuy ? tp.quoteAsk() : tp.quoteBid()) : order.price; - const { outboundCurrency, inboundCurrency, outboundAmount, inboundAmount } = - Swaps.calculateInboundOutboundAmounts(order.quantity, 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); - } - if (!inboundSwapClient) { - throw swapsErrors.SWAP_CLIENT_NOT_FOUND(inboundCurrency); - } - - // check if sufficient outbound channel capacity exists - const totalOutboundAmount = outboundSwapClient.totalOutboundAmount(outboundCurrency); - if (outboundAmount > totalOutboundAmount) { - throw errors.INSUFFICIENT_OUTBOUND_BALANCE(outboundCurrency, outboundAmount, totalOutboundAmount); - } - - // check if sufficient inbound channel capacity exists - inboundSwapClient.checkInboundCapacity(inboundAmount, inboundCurrency); + await this.swaps.swapClientManager.checkSwapCapacities({ ...order, price }); } let replacedOrderIdentifier: OrderIdentifier | undefined; diff --git a/lib/orderbook/errors.ts b/lib/orderbook/errors.ts index e9c674bb3..e48278b59 100644 --- a/lib/orderbook/errors.ts +++ b/lib/orderbook/errors.ts @@ -13,7 +13,6 @@ const errorCodes = { LOCAL_ID_DOES_NOT_EXIST: codesPrefix.concat('.9'), QUANTITY_DOES_NOT_MATCH: codesPrefix.concat('.10'), CURRENCY_MISSING_ETHEREUM_CONTRACT_ADDRESS: codesPrefix.concat('.11'), - INSUFFICIENT_OUTBOUND_BALANCE: codesPrefix.concat('.12'), MIN_QUANTITY_VIOLATED: codesPrefix.concat('.13'), QUANTITY_ON_HOLD: codesPrefix.concat('.15'), DUPLICATE_PAIR_CURRENCIES: codesPrefix.concat('.16'), @@ -64,10 +63,6 @@ const errors = { message: `requestedQuantity: ${requestedQuantity} is higher than order quantity: ${orderQuantity}`, code: errorCodes.QUANTITY_DOES_NOT_MATCH, }), - INSUFFICIENT_OUTBOUND_BALANCE: (currency: string, amount: number, availableAmount: number) => ({ - message: `${currency} outbound balance of ${availableAmount} is not sufficient for order amount of ${amount}`, - code: errorCodes.INSUFFICIENT_OUTBOUND_BALANCE, - }), MIN_QUANTITY_VIOLATED: (quantity: number, currency: string) => ({ message: `order does not meet the minimum ${currency} quantity of ${quantity} satoshis`, code: errorCodes.MIN_QUANTITY_VIOLATED, diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index 30cd29795..8eb61526e 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -126,12 +126,6 @@ abstract class SwapClient extends EventEmitter { */ public abstract swapCapacities(currency?: string): Promise; - public abstract totalOutboundAmount(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; public abstract setReservedInboundAmount(reservedInboundAmount: number, currency?: string): void; protected abstract updateCapacity(): Promise; diff --git a/lib/swaps/SwapClientManager.ts b/lib/swaps/SwapClientManager.ts index ee37edd60..139642a98 100644 --- a/lib/swaps/SwapClientManager.ts +++ b/lib/swaps/SwapClientManager.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import { EventEmitter } from 'events'; import Config from '../Config'; import ConnextClient from '../connextclient/ConnextClient'; @@ -6,13 +7,12 @@ import { Models } from '../db/DB'; import lndErrors from '../lndclient/errors'; import LndClient from '../lndclient/LndClient'; import { LndInfo } from '../lndclient/types'; -import { Loggers, Level } from '../Logger'; -import { Currency } from '../orderbook/types'; +import { Level, Loggers } from '../Logger'; +import { Currency, OwnLimitOrder } from '../orderbook/types'; import Peer from '../p2p/Peer'; import { UnitConverter } from '../utils/UnitConverter'; import errors from './errors'; import SwapClient, { ClientStatus } from './SwapClient'; -import assert from 'assert'; import { TradingLimits } from './types'; export function isConnextClient(swapClient: SwapClient): swapClient is ConnextClient { @@ -184,6 +184,51 @@ class SwapClientManager extends EventEmitter { } } + /** + * Checks whether a given order with would exceed our inbound or outbound swap + * capacities when taking into consideration reserved amounts for standing + * orders, throws an exception if so and returns otherwise. + */ + public checkSwapCapacities = async ({ quantity, price, isBuy, pairId }: OwnLimitOrder) => { + const { outboundCurrency, inboundCurrency, outboundAmount, inboundAmount } = + UnitConverter.calculateInboundOutboundAmounts(quantity, price, isBuy, pairId); + + // check if clients exists + const outboundSwapClient = this.get(outboundCurrency); + const inboundSwapClient = this.get(inboundCurrency); + if (!outboundSwapClient) { + throw errors.SWAP_CLIENT_NOT_FOUND(outboundCurrency); + } + if (!inboundSwapClient) { + throw errors.SWAP_CLIENT_NOT_FOUND(inboundCurrency); + } + + const outboundCapacities = await outboundSwapClient.swapCapacities(outboundCurrency); + + // check if sufficient outbound channel capacity exists + const reservedOutbound = this.outboundReservedAmounts.get(outboundCurrency) ?? 0; + const availableOutboundCapacity = outboundCapacities.totalOutboundCapacity - reservedOutbound; + if (outboundAmount > availableOutboundCapacity) { + throw errors.INSUFFICIENT_OUTBOUND_CAPACITY(outboundCurrency, outboundAmount, availableOutboundCapacity); + } + + // check if sufficient inbound channel capacity exists + if (isConnextClient(inboundSwapClient)) { + // connext has the unique ability to dynamically request additional inbound capacity aka collateral + // we handle it differently and allow "lazy collateralization" if the total inbound capacity would + // be exceeded when including the reserved inbound amounts, we only reject if this order alone would + // exceed our inbound capacity + inboundSwapClient.checkInboundCapacity(inboundAmount, inboundCurrency); + } else { + const inboundCapacities = await inboundSwapClient.swapCapacities(inboundCurrency); + const reservedInbound = this.inboundReservedAmounts.get(inboundCurrency) ?? 0; + const availableInboundCapacity = inboundCapacities.totalInboundCapacity - reservedInbound; + if (inboundAmount > availableInboundCapacity) { + throw errors.INSUFFICIENT_INBOUND_CAPACITY(outboundCurrency, outboundAmount, availableOutboundCapacity); + } + } + } + public getOutboundReservedAmount = (currency: string) => { return this.outboundReservedAmounts.get(currency); } diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index 9648e02bb..2abb24ca4 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -10,6 +10,7 @@ import { PacketType } from '../p2p/packets'; import * as packets from '../p2p/packets/types'; import Peer from '../p2p/Peer'; import Pool from '../p2p/Pool'; +import { UnitConverter } from '../utils/UnitConverter'; import { generatePreimageAndHash, setTimeoutPromise } from '../utils/utils'; import errors, { errorCodes } from './errors'; import SwapClient, { PaymentState } from './SwapClient'; @@ -48,17 +49,6 @@ class Swaps extends EventEmitter { private timeouts = new Map(); private usedHashes = new Set(); private repository: SwapRepository; - /** Number of smallest units per currency. */ - // TODO: Use UnitConverter class instead - private static readonly UNITS_PER_CURRENCY: { [key: string]: number } = { - BTC: 1, - LTC: 1, - ETH: 10 ** 10, - USDT: 10 ** -2, - WETH: 10 ** 10, - DAI: 10 ** 10, - XUC: 10 ** 10, - }; /** The maximum time in milliseconds we will wait for a swap to be accepted before failing it. */ private static readonly SWAP_ACCEPT_TIMEOUT = 10000; /** The maximum time in milliseconds we will wait for a swap to be completed before failing it. */ @@ -141,7 +131,7 @@ class Swaps extends EventEmitter { */ private static calculateMakerTakerAmounts = (quantity: number, price: number, isBuy: boolean, pairId: string) => { const { inboundCurrency, inboundAmount, inboundUnits, outboundCurrency, outboundAmount, outboundUnits } = - Swaps.calculateInboundOutboundAmounts(quantity, price, isBuy, pairId); + UnitConverter.calculateInboundOutboundAmounts(quantity, price, isBuy, pairId); return { makerCurrency: inboundCurrency, makerAmount: inboundAmount, @@ -152,32 +142,6 @@ class Swaps extends EventEmitter { }; } - /** - * Calculates the incoming and outgoing currencies and amounts of subunits/satoshis for an order if it is swapped. - * @param quantity The quantity of the order - * @param price The price of the order - * @param isBuy Whether the order is a buy - * @returns An object with the calculated incoming and outgoing values. The quote currency - * amount is returned as zero if the price is 0 or infinity, indicating a market order. - */ - public static calculateInboundOutboundAmounts = (quantity: number, price: number, isBuy: boolean, pairId: string) => { - const [baseCurrency, quoteCurrency] = pairId.split('/'); - const baseCurrencyAmount = quantity; - const quoteCurrencyAmount = price > 0 && price < Number.POSITIVE_INFINITY ? - Math.round(quantity * price) : - 0; // if price is zero or infinity, this is a market order and we can't know the quote currency amount - const baseCurrencyUnits = Math.floor(baseCurrencyAmount * Swaps.UNITS_PER_CURRENCY[baseCurrency]); - const quoteCurrencyUnits = Math.floor(quoteCurrencyAmount * Swaps.UNITS_PER_CURRENCY[quoteCurrency]); - - const inboundCurrency = isBuy ? baseCurrency : quoteCurrency; - const inboundAmount = isBuy ? baseCurrencyAmount : quoteCurrencyAmount; - const inboundUnits = isBuy ? baseCurrencyUnits : quoteCurrencyUnits; - const outboundCurrency = isBuy ? quoteCurrency : baseCurrency; - const outboundAmount = isBuy ? quoteCurrencyAmount : baseCurrencyAmount; - const outboundUnits = isBuy ? quoteCurrencyUnits : baseCurrencyUnits; - return { inboundCurrency, inboundAmount, inboundUnits, outboundCurrency, outboundAmount, outboundUnits }; - } - public init = async () => { // update pool with lnd pubkeys this.swapClientManager.getLndClientsMap().forEach(({ pubKey, chain, currency, uris }) => { diff --git a/lib/swaps/errors.ts b/lib/swaps/errors.ts index 1602ab426..c5196d278 100644 --- a/lib/swaps/errors.ts +++ b/lib/swaps/errors.ts @@ -14,6 +14,8 @@ const errorCodes = { PAYMENT_PENDING: codesPrefix.concat('.10'), REMOTE_IDENTIFIER_MISSING: codesPrefix.concat('.11'), INSUFFICIENT_BALANCE: codesPrefix.concat('.12'), + INSUFFICIENT_OUTBOUND_CAPACITY: codesPrefix.concat('.13'), + INSUFFICIENT_INBOUND_CAPACITY: codesPrefix.concat('.14'), }; const errors = { @@ -70,6 +72,14 @@ const errors = { message: 'swap failed due to insufficient channel balance', code: errorCodes.INSUFFICIENT_BALANCE, }, + INSUFFICIENT_OUTBOUND_CAPACITY: (currency: string, amount: number, capacity: number) => ({ + message: `${currency} outbound capacity of ${Math.max(0, capacity)} is not sufficient for order amount of ${amount}`, + code: errorCodes.INSUFFICIENT_OUTBOUND_CAPACITY, + }), + INSUFFICIENT_INBOUND_CAPACITY: (currency: string, amount: number, capacity: number) => ({ + message: `${currency} inbound capacity of ${Math.max(0, capacity)} is not sufficient for order amount of ${amount}`, + code: errorCodes.INSUFFICIENT_INBOUND_CAPACITY, + }), }; export { errorCodes }; diff --git a/lib/utils/UnitConverter.ts b/lib/utils/UnitConverter.ts index 57c9c2d26..7543af36d 100644 --- a/lib/utils/UnitConverter.ts +++ b/lib/utils/UnitConverter.ts @@ -1,14 +1,43 @@ +const UNITS_PER_CURRENCY: { [key: string]: number } = { + BTC: 1, + LTC: 1, + ETH: 10 ** 10, + USDT: 10 ** -2, + WETH: 10 ** 10, + DAI: 10 ** 10, + XUC: 10 ** 10, +}; + class UnitConverter { /** Number of smallest units per currency. */ - private UNITS_PER_CURRENCY: { [key: string]: number } = { - BTC: 1, - LTC: 1, - ETH: 10 ** 10, - USDT: 10 ** -2, - WETH: 10 ** 10, - DAI: 10 ** 10, - XUC: 10 ** 10, - }; + private UNITS_PER_CURRENCY: { [key: string]: number } = UNITS_PER_CURRENCY; + + /** + * Calculates the incoming and outgoing currencies and amounts of subunits/satoshis for an order if it is swapped. + * @param quantity The quantity of the order + * @param price The price of the order + * @param isBuy Whether the order is a buy + * @returns An object with the calculated incoming and outgoing values. The quote currency + * amount is returned as zero if the price is 0 or infinity, indicating a market order. + */ + public static calculateInboundOutboundAmounts = (quantity: number, price: number, isBuy: boolean, pairId: string) => { + const [baseCurrency, quoteCurrency] = pairId.split('/'); + const baseCurrencyAmount = quantity; + const quoteCurrencyAmount = price > 0 && price < Number.POSITIVE_INFINITY ? + Math.round(quantity * price) : + 0; // if price is zero or infinity, this is a market order and we can't know the quote currency amount + const baseCurrencyUnits = Math.floor(baseCurrencyAmount * UNITS_PER_CURRENCY[baseCurrency]); + const quoteCurrencyUnits = Math.floor(quoteCurrencyAmount * UNITS_PER_CURRENCY[quoteCurrency]); + + const inboundCurrency = isBuy ? baseCurrency : quoteCurrency; + const inboundAmount = isBuy ? baseCurrencyAmount : quoteCurrencyAmount; + const inboundUnits = isBuy ? baseCurrencyUnits : quoteCurrencyUnits; + const outboundCurrency = isBuy ? quoteCurrency : baseCurrency; + const outboundAmount = isBuy ? quoteCurrencyAmount : baseCurrencyAmount; + const outboundUnits = isBuy ? quoteCurrencyUnits : baseCurrencyUnits; + return { inboundCurrency, inboundAmount, inboundUnits, outboundCurrency, outboundAmount, outboundUnits }; + } + public init = () => { // TODO: Populate the mapping from the database (Currency.decimalPlaces). // this.UNITS_PER_CURRENCY = await fetchUnitsPerCurrencyFromDatabase(); diff --git a/test/jest/Orderbook.spec.ts b/test/jest/Orderbook.spec.ts index b89944f97..8be87dcbf 100644 --- a/test/jest/Orderbook.spec.ts +++ b/test/jest/Orderbook.spec.ts @@ -100,6 +100,7 @@ jest.mock('../../lib/swaps/SwapClientManager', () => { return { canRouteToPeer: jest.fn().mockReturnValue(true), isConnected: jest.fn().mockReturnValue(true), + checkSwapCapacities: jest.fn(), get: jest.fn().mockReturnValue({ maximumOutboundCapacity: () => Number.MAX_SAFE_INTEGER }), }; }); @@ -222,77 +223,7 @@ describe('OrderBook', () => { }); describe('placeOrder', () => { - test('insufficient outbound balance throws when balancechecks enabled', async () => { - orderbook['nobalancechecks'] = false; - const quantity = 10000; - const price = 0.01; - const order: OwnLimitOrder = { - quantity, - pairId, - price, - localId, - isBuy: false, - }; - Swaps['calculateInboundOutboundAmounts'] = () => { - return { - inboundCurrency: 'BTC', - inboundAmount: quantity * price, - inboundUnits: quantity * price, - outboundCurrency: 'LTC', - outboundAmount: quantity, - outboundUnits: quantity, - }; - }; - swaps.swapClientManager.get = jest.fn().mockReturnValue({ - totalOutboundAmount: () => 1, - }); - await expect(orderbook.placeLimitOrder({ order })) - .rejects.toMatchSnapshot(); - }); - - test('checks swap client for insufficient inbound balance when balancechecks enabled', async () => { - orderbook['nobalancechecks'] = false; - const quantity = 10000; - const price = 0.01; - const isBuy = false; - const order: OwnLimitOrder = { - quantity, - pairId, - price, - localId, - isBuy, - }; - const inboundCurrency = 'BTC'; - const outboundCurrency = 'LTC'; - const inboundAmount = quantity * price; - Swaps['calculateInboundOutboundAmounts'] = jest.fn().mockReturnValue({ - inboundCurrency, - outboundCurrency, - inboundAmount, - inboundUnits: inboundAmount, - outboundAmount: quantity, - outboundUnits: quantity, - }); - const inboundSwapClient = { - checkInboundCapacity: jest.fn(), - }; - swaps.swapClientManager.get = jest.fn().mockImplementation((currency) => { - if (currency === inboundCurrency) { - return inboundSwapClient; - } else if (currency === outboundCurrency) { - return { totalOutboundAmount: () => quantity }; - } - throw 'unexpected currency'; - }); - - await orderbook.placeLimitOrder({ order }); - expect(Swaps['calculateInboundOutboundAmounts']).toHaveBeenCalledWith(quantity, price, isBuy, pairId); - expect(inboundSwapClient.checkInboundCapacity).toHaveBeenCalledWith(inboundAmount, inboundCurrency); - expect(swaps.swapClientManager.addInboundReservedAmount).toHaveBeenCalledWith('BTC', quantity * price); - expect(swaps.swapClientManager.addOutboundReservedAmount).toHaveBeenCalledWith('LTC', quantity); - }); - - test('market order checks swap client for insufficient inbound balance using best quoted price', async () => { + test('market order checks swap clients for insufficient inbound balance using best quoted price', async () => { const quantity = 20000000; const price = 4000; const usdtPairId = 'BTC/USDT'; @@ -315,33 +246,13 @@ describe('OrderBook', () => { isBuy, pairId: usdtPairId, }; - const inboundCurrency = 'USDT'; - const outboundCurrency = 'BTC'; - const inboundAmount = quantity * price; - Swaps['calculateInboundOutboundAmounts'] = jest.fn().mockReturnValue({ - inboundCurrency, - outboundCurrency, - inboundAmount, - inboundUnits: inboundAmount * 10 ** 10, - outboundAmount: quantity, - outboundUnits: quantity, - }); - const inboundSwapClient = { - checkInboundCapacity: jest.fn(), - }; - swaps.swapClientManager.get = jest.fn().mockImplementation((currency) => { - if (currency === inboundCurrency) { - return inboundSwapClient; - } else if (currency === outboundCurrency) { - return { totalOutboundAmount: () => quantity }; - } - throw 'unexpected currency'; - }); + swaps.swapClientManager.checkSwapCapacities = jest.fn(); await orderbook.placeMarketOrder({ order }); - expect(Swaps['calculateInboundOutboundAmounts']).toHaveBeenCalledWith(quantity, price, isBuy, usdtPairId); - expect(inboundSwapClient.checkInboundCapacity).toHaveBeenCalledWith(inboundAmount, inboundCurrency); + expect(swaps.swapClientManager.checkSwapCapacities).toHaveBeenCalledWith( + expect.objectContaining({ ...order, price }), + ); }); test('placeLimitOrder adds to order book', async () => { @@ -354,18 +265,7 @@ describe('OrderBook', () => { price, isBuy: false, }; - const inboundCurrency = 'BTC'; - const outboundCurrency = 'LTC'; - const inboundAmount = quantity * price; - const outboundAmount = quantity; - Swaps['calculateInboundOutboundAmounts'] = jest.fn().mockReturnValue({ - inboundCurrency, - outboundCurrency, - inboundAmount, - outboundAmount, - inboundUnits: inboundAmount, - outboundUnits: outboundAmount, - }); + await orderbook.placeLimitOrder({ order }); expect(orderbook.getOwnOrderByLocalId(localId)).toHaveProperty('localId', localId); expect(swaps.swapClientManager.addInboundReservedAmount).toHaveBeenCalledWith('BTC', quantity * price); diff --git a/test/jest/SwapClientManager.spec.ts b/test/jest/SwapClientManager.spec.ts index d1c33d9c1..1308be9f1 100644 --- a/test/jest/SwapClientManager.spec.ts +++ b/test/jest/SwapClientManager.spec.ts @@ -5,6 +5,8 @@ import Logger from '../../lib/Logger'; import SwapClient from '../../lib/swaps/SwapClient'; import SwapClientManager from '../../lib/swaps/SwapClientManager'; import { UnitConverter } from '../../lib/utils/UnitConverter'; +import { OwnLimitOrder } from '../../lib/orderbook/types'; +import { errorCodes } from '../../lib/swaps/errors'; jest.mock('../../lib/db/DB', () => { return jest.fn().mockImplementation(() => { @@ -279,16 +281,16 @@ describe('Swaps.SwapClientManager', () => { }); }); - describe('tradingLimits', () => { - const setup = () => { + describe('tradingLimits & checkSwapCapacities', () => { + beforeAll(() => { const btcClient = new mockedSwapClient(); btcClient.isConnected = jest.fn().mockImplementation(() => true); btcClient.swapCapacities = jest.fn().mockImplementation(() => { return Promise.resolve({ - maxOutboundChannelCapacity: 2000, - maxInboundChannelCapacity: 1500, - totalOutboundCapacity: 2000, - totalInboundCapacity: 1500, + maxOutboundChannelCapacity: 200000, + maxInboundChannelCapacity: 150000, + totalOutboundCapacity: 200000, + totalInboundCapacity: 150000, }); }); swapClientManager.swapClients.set('BTC', btcClient); @@ -297,31 +299,103 @@ describe('Swaps.SwapClientManager', () => { ltcClient.isConnected = jest.fn().mockImplementation(() => true); ltcClient.swapCapacities = jest.fn().mockImplementation(() => { return Promise.resolve({ - maxOutboundChannelCapacity: 7000, - maxInboundChannelCapacity: 5500, - totalOutboundCapacity: 7000, - totalInboundCapacity: 5500, + maxOutboundChannelCapacity: 700000, + maxInboundChannelCapacity: 550000, + totalOutboundCapacity: 700000, + totalInboundCapacity: 550000, }); }); swapClientManager.swapClients.set('LTC', ltcClient); - const bchClient = new mockedSwapClient(); - bchClient.isConnected = jest.fn().mockImplementation(() => false); - swapClientManager.swapClients.set('BCH', bchClient); - }; + }); - test('returns trading limits', async () => { - setup(); + test('returns trading limits for a currency', async () => { const btcTradingLimits = await swapClientManager.tradingLimits('BTC'); expect(btcTradingLimits).toBeTruthy(); - expect(btcTradingLimits.maxSell).toEqual(2000); - expect(btcTradingLimits.maxBuy).toEqual(1500); + expect(btcTradingLimits.maxSell).toEqual(200000); + expect(btcTradingLimits.maxBuy).toEqual(150000); + }); + + test('throws if outbound swap capacity is insufficient for an order', async () => { + const order: OwnLimitOrder = { + price: 0.01, + pairId: 'LTC/BTC', + quantity: 800000, + isBuy: false, + localId: 'test', + }; + + await expect(swapClientManager.checkSwapCapacities(order)).rejects.toHaveProperty('code', errorCodes.INSUFFICIENT_OUTBOUND_CAPACITY); + }); + + test('throws if inbound swap capacity is insufficient for an order', async () => { + const order: OwnLimitOrder = { + price: 1, + pairId: 'LTC/BTC', + quantity: 300000, + isBuy: false, + localId: 'test', + }; + + await expect(swapClientManager.checkSwapCapacities(order)).rejects.toHaveProperty('code', errorCodes.INSUFFICIENT_INBOUND_CAPACITY); + }); + + test('throws if outbound swap capacity is insufficient for an order plus reserved amounts', async () => { + const order: OwnLimitOrder = { + price: 0.01, + pairId: 'LTC/BTC', + quantity: 600000, + isBuy: false, + localId: 'test', + }; + + swapClientManager['outboundReservedAmounts'].set('LTC', 200000); + + await expect(swapClientManager.checkSwapCapacities(order)).rejects.toHaveProperty('code', errorCodes.INSUFFICIENT_OUTBOUND_CAPACITY); + }); + + test('throws if inbound swap capacity is insufficient for an order plus reserved amounts', async () => { + const order: OwnLimitOrder = { + price: 1, + pairId: 'LTC/BTC', + quantity: 100000, + isBuy: false, + localId: 'test', + }; + + swapClientManager['inboundReservedAmounts'].set('BTC', 200000); + + await expect(swapClientManager.checkSwapCapacities(order)).rejects.toHaveProperty('code', errorCodes.INSUFFICIENT_INBOUND_CAPACITY); + }); + + test('returns if swap capacity is sufficient', async () => { + const order: OwnLimitOrder = { + price: 0.01, + pairId: 'LTC/BTC', + quantity: 600000, + isBuy: false, + localId: 'test', + }; + + swapClientManager['outboundReservedAmounts'].set('LTC', 50000); + swapClientManager['inboundReservedAmounts'].set('BTC', 50000); + + await swapClientManager.checkSwapCapacities(order); }); test('throws when swap client is not found', async () => { - setup(); - await expect(swapClientManager.tradingLimits('BBB')).rejects.toMatchSnapshot(); + await expect(swapClientManager.tradingLimits('BBB')).rejects.toHaveProperty('code', errorCodes.SWAP_CLIENT_NOT_FOUND); + + const order: OwnLimitOrder = { + price: 0.01, + pairId: 'AAA/BBB', + quantity: 600000, + isBuy: false, + localId: 'test', + }; + + await expect(swapClientManager.checkSwapCapacities(order)).rejects.toHaveProperty('code', errorCodes.SWAP_CLIENT_NOT_FOUND); }); }); }); diff --git a/test/jest/UnitConverter.spec.ts b/test/jest/UnitConverter.spec.ts index 27eb15734..1fc86c916 100644 --- a/test/jest/UnitConverter.spec.ts +++ b/test/jest/UnitConverter.spec.ts @@ -1,11 +1,11 @@ import { UnitConverter } from '../../lib/utils/UnitConverter'; describe('UnitConverter', () => { + const unitConverter = new UnitConverter(); describe('amountToUnits', () => { test('converts BTC amount to units', () => { - const unitConverter = new UnitConverter(); unitConverter.init(); const amount = 99999999; expect(unitConverter. @@ -17,7 +17,6 @@ describe('UnitConverter', () => { }); test('converts WETH amount to units', () => { - const unitConverter = new UnitConverter(); unitConverter.init(); expect(unitConverter. amountToUnits({ @@ -29,7 +28,6 @@ describe('UnitConverter', () => { test('throws error upon unknown currency', () => { expect.assertions(1); - const unitConverter = new UnitConverter(); unitConverter.init(); try { unitConverter.amountToUnits({ @@ -46,7 +44,6 @@ describe('UnitConverter', () => { describe('unitsToAmount', () => { test('converts BTC units to amount', () => { - const unitConverter = new UnitConverter(); unitConverter.init(); const units = 99999999; expect(unitConverter. @@ -58,7 +55,6 @@ describe('UnitConverter', () => { }); test('converts WETH units to amount', () => { - const unitConverter = new UnitConverter(); unitConverter.init(); expect(unitConverter. unitsToAmount({ @@ -70,7 +66,6 @@ describe('UnitConverter', () => { test('throws error upon unknown currency', () => { expect.assertions(1); - const unitConverter = new UnitConverter(); unitConverter.init(); try { unitConverter.unitsToAmount({ @@ -83,4 +78,59 @@ describe('UnitConverter', () => { }); }); + + describe('calculateSwapAmounts', () => { + const pairId = 'LTC/BTC'; + const quantity = 250000; + const price = 0.01; + + test('calculate inbound and outbound amounts and currencies for a buy order', () => { + const { inboundCurrency, inboundAmount, outboundCurrency, outboundAmount, inboundUnits, outboundUnits } = + UnitConverter.calculateInboundOutboundAmounts(quantity, price, true, pairId); + expect(inboundCurrency).toEqual('LTC'); + expect(inboundAmount).toEqual(quantity); + expect(inboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['LTC'] * quantity); + expect(outboundCurrency).toEqual('BTC'); + expect(outboundAmount).toEqual(quantity * price); + expect(outboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['BTC'] * quantity * price); + }); + + test('calculate inbound and outbound amounts and currencies for a sell order', () => { + const { inboundCurrency, inboundAmount, outboundCurrency, outboundAmount, inboundUnits, outboundUnits } = + UnitConverter.calculateInboundOutboundAmounts(quantity, price, false, pairId); + expect(inboundCurrency).toEqual('BTC'); + expect(inboundAmount).toEqual(quantity * price); + expect(inboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['BTC'] * quantity * price); + expect(outboundCurrency).toEqual('LTC'); + expect(outboundAmount).toEqual(quantity); + expect(outboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['LTC'] * quantity); + }); + + test('calculate 0 outbound amount for a market buy order', () => { + const { outboundCurrency, outboundAmount, outboundUnits } = + UnitConverter.calculateInboundOutboundAmounts(quantity, 0, true, pairId); + expect(outboundCurrency).toEqual('BTC'); + expect(outboundAmount).toEqual(0); + expect(outboundUnits).toEqual(0); + }); + + test('calculate 0 inbound amount for a market sell order', () => { + const { inboundCurrency, inboundAmount, inboundUnits } = + UnitConverter.calculateInboundOutboundAmounts(quantity, Number.POSITIVE_INFINITY, false, pairId); + expect(inboundCurrency).toEqual('BTC'); + expect(inboundAmount).toEqual(0); + expect(inboundUnits).toEqual(0); + }); + + test('calculate inbound and outbound amounts and currencies for a Connext order', () => { + const { inboundCurrency, inboundAmount, outboundCurrency, outboundAmount, inboundUnits, outboundUnits } = + UnitConverter.calculateInboundOutboundAmounts(quantity, price, true, 'ETH/BTC'); + expect(inboundCurrency).toEqual('ETH'); + expect(inboundAmount).toEqual(quantity); + expect(inboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['ETH'] * quantity); + expect(outboundCurrency).toEqual('BTC'); + expect(outboundAmount).toEqual(quantity * price); + expect(outboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['BTC'] * quantity * price); + }); + }); }); diff --git a/test/jest/__snapshots__/Orderbook.spec.ts.snap b/test/jest/__snapshots__/Orderbook.spec.ts.snap deleted file mode 100644 index f344c5d8f..000000000 --- a/test/jest/__snapshots__/Orderbook.spec.ts.snap +++ /dev/null @@ -1,8 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`OrderBook placeOrder insufficient outbound balance throws when balancechecks enabled 1`] = ` -Object { - "code": "3.12", - "message": "LTC outbound balance of 1 is not sufficient for order amount of 10000", -} -`; diff --git a/test/jest/__snapshots__/SwapClientManager.spec.ts.snap b/test/jest/__snapshots__/SwapClientManager.spec.ts.snap index 3b0f8aa61..4ec2a09a6 100644 --- a/test/jest/__snapshots__/SwapClientManager.spec.ts.snap +++ b/test/jest/__snapshots__/SwapClientManager.spec.ts.snap @@ -6,10 +6,3 @@ Object { "message": "swapClient for currency BTC not found", } `; - -exports[`Swaps.SwapClientManager tradingLimits throws when swap client is not found 1`] = ` -Object { - "code": "7.1", - "message": "swapClient for currency BBB not found", -} -`; diff --git a/test/unit/Swaps.spec.ts b/test/unit/Swaps.spec.ts index d6704beb3..9514d7e01 100644 --- a/test/unit/Swaps.spec.ts +++ b/test/unit/Swaps.spec.ts @@ -32,8 +32,8 @@ describe('Swaps', () => { takerCurrency: 'BTC', makerAmount: quantity, takerAmount: quantity * price, - makerUnits: Swaps['UNITS_PER_CURRENCY']['LTC'] * quantity, - takerUnits: Swaps['UNITS_PER_CURRENCY']['BTC'] * quantity * price, + makerUnits: quantity, + takerUnits: quantity * price, createTime: 1540716251106, }; @@ -44,8 +44,8 @@ describe('Swaps', () => { takerCurrency: 'BTC', makerAmount: quantity, takerAmount: quantity * price, - makerUnits: Swaps['UNITS_PER_CURRENCY']['WETH'] * quantity, - takerUnits: Swaps['UNITS_PER_CURRENCY']['BTC'] * quantity * price, + makerUnits: 10 ** 10 * quantity, + takerUnits: quantity * price, }; /** A swap deal for a sell order, mirrored from the buy deal for convenience. */ @@ -104,55 +104,6 @@ describe('Swaps', () => { expect(takerCurrency).to.equal(buyDealEth.takerCurrency); }); - it('should calculate inbound and outbound amounts and currencies for a buy order', () => { - const { inboundCurrency, inboundAmount, outboundCurrency, outboundAmount, inboundUnits, outboundUnits } = - Swaps.calculateInboundOutboundAmounts(quantity, price, true, pairId); - expect(inboundCurrency).to.equal('LTC'); - expect(inboundAmount).to.equal(quantity); - expect(inboundUnits).to.equal(Swaps['UNITS_PER_CURRENCY']['LTC'] * quantity); - expect(outboundCurrency).to.equal('BTC'); - expect(outboundAmount).to.equal(quantity * price); - expect(outboundUnits).to.equal(Swaps['UNITS_PER_CURRENCY']['BTC'] * quantity * price); - }); - - it('should calculate inbound and outbound amounts and currencies for a sell order', () => { - const { inboundCurrency, inboundAmount, outboundCurrency, outboundAmount, inboundUnits, outboundUnits } = - Swaps.calculateInboundOutboundAmounts(quantity, price, false, pairId); - expect(inboundCurrency).to.equal('BTC'); - expect(inboundAmount).to.equal(quantity * price); - expect(inboundUnits).to.equal(Swaps['UNITS_PER_CURRENCY']['BTC'] * quantity * price); - expect(outboundCurrency).to.equal('LTC'); - expect(outboundAmount).to.equal(quantity); - expect(outboundUnits).to.equal(Swaps['UNITS_PER_CURRENCY']['LTC'] * quantity); - }); - - it('should calculate 0 outbound amount for a market buy order', () => { - const { outboundCurrency, outboundAmount, outboundUnits } = - Swaps.calculateInboundOutboundAmounts(quantity, 0, true, pairId); - expect(outboundCurrency).to.equal('BTC'); - expect(outboundAmount).to.equal(0); - expect(outboundUnits).to.equal(0); - }); - - it('should calculate 0 inbound amount for a market sell order', () => { - const { inboundCurrency, inboundAmount, inboundUnits } = - Swaps.calculateInboundOutboundAmounts(quantity, Number.POSITIVE_INFINITY, false, pairId); - expect(inboundCurrency).to.equal('BTC'); - expect(inboundAmount).to.equal(0); - expect(inboundUnits).to.equal(0); - }); - - it('should calculate inbound and outbound amounts and currencies for a Connext order', () => { - const { inboundCurrency, inboundAmount, outboundCurrency, outboundAmount, inboundUnits, outboundUnits } = - Swaps.calculateInboundOutboundAmounts(quantity, price, true, 'ETH/BTC'); - expect(inboundCurrency).to.equal('ETH'); - expect(inboundAmount).to.equal(quantity); - expect(inboundUnits).to.equal(Swaps['UNITS_PER_CURRENCY']['ETH'] * quantity); - expect(outboundCurrency).to.equal('BTC'); - expect(outboundAmount).to.equal(quantity * price); - expect(outboundUnits).to.equal(Swaps['UNITS_PER_CURRENCY']['BTC'] * quantity * price); - }); - it('should validate a good swap request', () => { expect(Swaps.validateSwapRequest(swapRequest)).to.be.true; });