Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: reserved capacity checks on PlaceOrder #1949

Merged
merged 1 commit into from
Oct 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions lib/connextclient/ConnextClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion lib/grpc/getGrpcError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
14 changes: 3 additions & 11 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class LndClient extends SwapClient {
private channelBackupSubscription?: ClientReadableStream<lndrpc.ChanBackupSnapshot>;
private invoiceSubscriptions = new Map<string, ClientReadableStream<lndrpc.Invoice>>();
private initRetryTimeout?: NodeJS.Timeout;
private _totalOutboundAmount = 0;
private totalOutboundAmount = 0;
private totalInboundAmount = 0;
private maxChannelOutboundAmount = 0;
private maxChannelInboundAmount = 0;
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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}`);
}

Expand Down
28 changes: 4 additions & 24 deletions lib/orderbook/OrderBook.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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);
};
Expand All @@ -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);
});
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 0 additions & 5 deletions lib/orderbook/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 0 additions & 6 deletions lib/swaps/SwapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,6 @@ abstract class SwapClient extends EventEmitter {
*/
public abstract swapCapacities(currency?: string): Promise<SwapCapacities>;

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<void>;

Expand Down
51 changes: 48 additions & 3 deletions lib/swaps/SwapClientManager.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from 'assert';
import { EventEmitter } from 'events';
import Config from '../Config';
import ConnextClient from '../connextclient/ConnextClient';
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}
Expand Down
40 changes: 2 additions & 38 deletions lib/swaps/Swaps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -48,17 +49,6 @@ class Swaps extends EventEmitter {
private timeouts = new Map<string, number>();
private usedHashes = new Set<string>();
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. */
Expand Down Expand Up @@ -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,
Expand All @@ -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 }) => {
Expand Down
10 changes: 10 additions & 0 deletions lib/swaps/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 };
Expand Down
47 changes: 38 additions & 9 deletions lib/utils/UnitConverter.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Loading