Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/ExchangeUnion/xud into fe…
Browse files Browse the repository at this point in the history
…ature/buy-sell-all-grpc
  • Loading branch information
rsercano committed Oct 3, 2020
2 parents 8155d93 + bd0210e commit 1217930
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 204 deletions.
37 changes: 11 additions & 26 deletions lib/db/DB.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { promises as fs } from 'fs';
import { derivePairId } from '../utils/utils';
import { ModelCtor, Sequelize } from 'sequelize';
import { XuNetwork } from '../constants/enums';
Expand Down Expand Up @@ -137,8 +136,6 @@ class DB {
* @param initDb whether to intialize a new database with default values if no database exists
*/
public init = async (network = XuNetwork.SimNet, initDb = false): Promise<void> => {
const shouldInitDb = initDb && await this.isNewDb();

try {
await this.sequelize.authenticate();
this.logger.info(`connected to database ${this.storage ? this.storage : 'in memory'}`);
Expand Down Expand Up @@ -177,41 +174,29 @@ class DB {
await Node.bulkCreate(newNodes);
}
}
}

if (shouldInitDb) {
// initialize database with the default currencies for the configured network
const currencies = defaultCurrencies(network);
if (currencies) {
await Currency.bulkCreate(currencies);
const existingCurrencies = await Models.Currency(this.sequelize).findAll();
const newCurrencies = currencies.filter(currency => (!existingCurrencies.find(n => (n.id === currency.id))));

if (newCurrencies.length > 0) {
await Currency.bulkCreate(newCurrencies);
}
}

// initialize database with the default trading pairs for the configured network
const pairs = defaultPairs(network);
if (pairs) {
await Pair.bulkCreate(pairs);
}
}
}
const existingPairs = await Models.Pair(this.sequelize).findAll();
const newPairs = pairs.filter(pair => (!existingPairs.find(n => (n.baseCurrency === pair.baseCurrency &&
n.quoteCurrency === pair.quoteCurrency))));

/**
* Checks whether the database is new, in other words whether we are not
* loading a preexisting database from disk.
*/
private isNewDb = async () => {
if (this.storage && this.storage !== ':memory:') {
// check if database file exists
try {
await fs.access(this.storage);
return false;
} catch (err) {
if (err.code !== 'ENOENT') {
// we ignore errors due to file not existing, otherwise throw
throw err;
if (newPairs.length > 0) {
await Pair.bulkCreate(newPairs);
}
}
}
return true;
}

public close = () => {
Expand Down
21 changes: 20 additions & 1 deletion lib/lndclient/LndClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from 'assert';
import crypto from 'crypto';
import { promises as fs, watch } from 'fs';
import grpc, { ChannelCredentials, ClientReadableStream } from 'grpc';
import path from 'path';
Expand Down Expand Up @@ -242,6 +243,10 @@ class LndClient extends SwapClient {

private unaryCall = <T, U>(methodName: Exclude<keyof LightningClient, ClientMethods>, params: T): Promise<U> => {
return new Promise((resolve, reject) => {
if (this.hasNoInvoiceSupport()) {
reject(errors.NO_HOLD_INVOICE_SUPPORT);
return;
}
if (!this.isOperational()) {
reject(errors.DISABLED);
return;
Expand Down Expand Up @@ -337,7 +342,9 @@ class LndClient extends SwapClient {
let version: string | undefined;
let alias: string | undefined;
let status = 'Ready';
if (!this.isOperational()) {
if (this.hasNoInvoiceSupport()) {
status = errors.NO_HOLD_INVOICE_SUPPORT(this.currency).message;
} else if (!this.isOperational()) {
status = errors.DISABLED(this.currency).message;
} else if (this.isDisconnected()) {
status = errors.UNAVAILABLE(this.currency, this.status).message;
Expand Down Expand Up @@ -493,6 +500,18 @@ class LndClient extends SwapClient {
}

this.invoices = new InvoicesClient(this.uri, this.credentials);
try {
const randomHash = crypto.randomBytes(32).toString('hex');
this.logger.debug(`checking hold invoice support with hash: ${randomHash}`);

await this.addInvoice({ rHash: randomHash, units: 1 });
await this.removeInvoice(randomHash);
} catch (err) {
const errStr = typeof(err) === 'string' ? err : JSON.stringify(err);

this.logger.error(`could not add hold invoice, error: ${errStr}`);
this.setStatus(ClientStatus.NoHoldInvoiceSupport);
}

if (this.walletUnlocker) {
// WalletUnlocker service is disabled when the main Lightning service is available
Expand Down
5 changes: 5 additions & 0 deletions lib/lndclient/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const errorCodes = {
DISABLED: codesPrefix.concat('.1'),
UNAVAILABLE: codesPrefix.concat('.2'),
NO_ACTIVE_CHANNELS: codesPrefix.concat('.3'),
NO_HOLD_INVOICE_SUPPORT: codesPrefix.concat('.4'),
};

const errors = {
Expand All @@ -21,6 +22,10 @@ const errors = {
message: `lnd-${currency} has no active channels`,
code: errorCodes.NO_ACTIVE_CHANNELS,
}),
NO_HOLD_INVOICE_SUPPORT: (currency: string) => ({
message: `lnd-${currency} has no hold invoice support`,
code: errorCodes.NO_HOLD_INVOICE_SUPPORT,
}),
};

export { errorCodes };
Expand Down
34 changes: 32 additions & 2 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,8 +626,14 @@ class Service {
const currency = pairId.split('/')[0];
calculatedQuantity = (await this.getBalance({ currency })).get(currency)?.channelBalance || 0;
} else {
// TODO
calculatedQuantity = 0;
const currency = pairId.split('/')[1];
const balance = (await this.getBalance({ currency })).get(currency)?.channelBalance || 0;

if (!price) {
calculatedQuantity = this.calculateBuyMaxMarketQuantity(pairId, balance);
} else {
calculatedQuantity = balance / price;
}
}

this.logger.debug(`max flag is true to place order, calculated quantity from balance is ${calculatedQuantity}`);
Expand Down Expand Up @@ -664,6 +670,30 @@ class Service {
await this.orderBook.placeMarketOrder(placeOrderRequest);
}

private calculateBuyMaxMarketQuantity(pairId: string, balance: number) {
let result = 0;
let currentBalance = balance;

this.listOrders({ pairId, owner: Owner.Both, limit: 0, includeAliases: false }).forEach((orderArrays, _) => {
for (const order of orderArrays.sellArray) {
if (order.quantity && order.price) {
// market buy max calculation
const maxBuyableFromThisPrice = currentBalance / order.price;
const calculatedQuantity = (maxBuyableFromThisPrice > order.quantity) ? order.quantity : maxBuyableFromThisPrice;
result += calculatedQuantity;
currentBalance -= order.price * calculatedQuantity;

if (currentBalance === 0) {
// we filled our buy quantity with this order
break;
}
}
}
});

return result;
}

/** Removes a currency. */
public removeCurrency = async (args: { currency: string }) => {
argChecks.VALID_CURRENCY(args);
Expand Down
8 changes: 7 additions & 1 deletion lib/swaps/SwapClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ enum ClientStatus {
Unlocked,
/** The client could not be initialized due to faulty configuration. */
Misconfigured,
/** The server is reachable but hold invoices are not supported. */
NoHoldInvoiceSupport,
}

type ChannelBalance = {
Expand Down Expand Up @@ -216,6 +218,7 @@ abstract class SwapClient extends EventEmitter {
case ClientStatus.Disconnected:
case ClientStatus.WaitingUnlock:
case ClientStatus.OutOfSync:
case ClientStatus.NoHoldInvoiceSupport:
// these statuses can only be set on an operational, initalized client
validStatusTransition = this.isOperational();
break;
Expand Down Expand Up @@ -359,7 +362,7 @@ abstract class SwapClient extends EventEmitter {
* Returns `true` if the client is enabled and configured properly.
*/
public isOperational(): boolean {
return !this.isDisabled() && !this.isMisconfigured() && !this.isNotInitialized();
return !this.isDisabled() && !this.isMisconfigured() && !this.isNotInitialized() && !this.hasNoInvoiceSupport();
}
public isDisconnected(): boolean {
return this.status === ClientStatus.Disconnected;
Expand All @@ -373,6 +376,9 @@ abstract class SwapClient extends EventEmitter {
public isOutOfSync(): boolean {
return this.status === ClientStatus.OutOfSync;
}
public hasNoInvoiceSupport(): boolean {
return this.status === ClientStatus.NoHoldInvoiceSupport;
}

/** Ends all connections, subscriptions, and timers for for this client. */
public close() {
Expand Down
61 changes: 61 additions & 0 deletions test/integration/Service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import chai, { expect } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
import { OrderSide, Owner, SwapClientType } from '../../lib/constants/enums';
import p2pErrors from '../../lib/p2p/errors';
import Service from '../../lib/service/Service';
import Xud from '../../lib/Xud';
import { getTempDir } from '../utils';
import { ServiceOrderSidesArrays } from '../../lib/service/types';

chai.use(chaiAsPromised);

Expand Down Expand Up @@ -186,4 +188,63 @@ describe('API Service', () => {
});
await expect(shutdownPromise).to.be.fulfilled;
});

describe('Max Quantity Calculation', () => {
before(async () => {
const map = new Map<string, ServiceOrderSidesArrays>();
map.set('BTC/DAI', {
buyArray: [],
sellArray: [
{ quantity: 0.01, price: 20000, pairId: 'BTC/DAI', id: 'test_1', createdAt: 1, side: OrderSide.Sell,
isOwnOrder: false, nodeIdentifier: { nodePubKey: 'some_key' } },
{ quantity: 0.01, price: 50000, pairId: 'BTC/DAI', id: 'test_2', createdAt: 1, side: OrderSide.Sell,
isOwnOrder: false, nodeIdentifier: { nodePubKey: 'some_key2' } },
{ quantity: 0.05, price: 100000, pairId: 'BTC/DAI', id: 'test_2', createdAt: 1, side: OrderSide.Sell,
isOwnOrder: false, nodeIdentifier: { nodePubKey: 'some_key2' } },
],
});

sinon.createSandbox().stub(service, 'listOrders').returns(map);
});

it('should return `0` for 0 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0, 0);
await expect(number).to.equal(0);
});

it('should return `0.005` for 100 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0, 100);
await expect(number).to.equal(0.005);
});

it('should return `0.01` for 200 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0, 200);
await expect(number).to.equal(0.01);
});

it('should return `0.016` for 500 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0, 500);
await expect(number).to.equal(0.016);
});

it('should return `0.02` for 700 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0, 700);
await expect(number).to.equal(0.02);
});

it('should return `0.021` for 800 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0, 800);
await expect(number).to.equal(0.021);
});

it('should return `0.07` for 5700 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0, 5700);
await expect(number).to.equal(0.07);
});

it('should return `0.07` for 10000 balance mkt', async () => {
const number = service['calculateBuyMaxMarketQuantity']('BTC/DAI', 0, 10000);
await expect(number).to.equal(0.07);
});
});
});
Loading

0 comments on commit 1217930

Please sign in to comment.