Skip to content

Commit

Permalink
feat(connext): request collateral for order amount
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
sangaman committed Sep 10, 2020
1 parent 62b9312 commit cb701b6
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 16 deletions.
39 changes: 32 additions & 7 deletions lib/connextclient/ConnextClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<string, Promise<ConnextBalanceResponse>>();
/** A map of currencies to promises representing collateral requests. */
private requestCollateralPromises = new Map<string, Promise<any>>();
private _totalOutboundAmount = new Map<string, number>();
private _maxChannelInboundAmount = new Map<string, number>();

/**
* Creates a connext client.
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
};
}

Expand Down
5 changes: 5 additions & 0 deletions lib/connextclient/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 };
Expand Down
1 change: 1 addition & 0 deletions lib/connextclient/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export type ConnextConfigResponse = {
*/
export type ConnextBalanceResponse = {
freeBalanceOffChain: string;
nodeFreeBalanceOffChain: string;
freeBalanceOnChain: string;
};

Expand Down
4 changes: 2 additions & 2 deletions lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
13 changes: 8 additions & 5 deletions lib/orderbook/OrderBook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
Expand Down
6 changes: 5 additions & 1 deletion lib/swaps/SwapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;

public verifyConnectionWithTimeout = () => {
Expand Down
2 changes: 1 addition & 1 deletion test/jest/LndClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});

0 comments on commit cb701b6

Please sign in to comment.