Skip to content
This repository has been archived by the owner on Aug 28, 2019. It is now read-only.

Commit

Permalink
feat: verify maximal and minimal amount
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Feb 9, 2019
1 parent 9740df3 commit dd0dd76
Show file tree
Hide file tree
Showing 12 changed files with 679 additions and 128 deletions.
8 changes: 7 additions & 1 deletion lib/BoltzMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ class BoltzMiddleware {
this.db = new Database(this.logger, this.config.dbpath);
this.boltzClient = new BoltzClient(this.logger, this.config.boltz);

this.service = new Service(this.logger, this.db, this.boltzClient, this.config.api.interval);
this.service = new Service(
this.logger,
this.boltzClient,
this.db,
this.config.api.interval,
this.config.currencies,
);

this.notifications = new NotificationProvider(
this.logger,
Expand Down
16 changes: 12 additions & 4 deletions lib/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,21 @@ class Config {
this.currencies = [
{
symbol: 'BTC',
walletbalance: 1000000,
channelbalance: 500000,

maxSwapAmount: 100000,
minSwapAmount: 1000,

minWalletBalance: 1000000,
minChannelBalance: 500000,
},
{
symbol: 'LTC',
walletbalance: 100000000,
channelbalance: 50000000,

maxSwapAmount: 10000000,
minSwapAmount: 10000,

minWalletBalance: 100000000,
minChannelBalance: 50000000,
},
];

Expand Down
9 changes: 8 additions & 1 deletion lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,12 @@ export const minutesToMilliseconds = (minutes: number) => {
* Convert satoshis to whole coins and remove trailing zeros
*/
export const satoshisToWholeCoins = (satoshis: number) => {
return Number((satoshis / 100000000).toFixed(8));
return roundToDecimals(satoshis / 100000000, 8);
};

/**
* Round a number to a specific amount of decimals
*/
export const roundToDecimals = (number: number, decimals: number) => {
return Number(number.toFixed(decimals));
};
1 change: 1 addition & 0 deletions lib/api/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class Api {

private registerRoutes = (controller: Controller) => {
this.app.route('/getpairs').get(controller.getPairs);
this.app.route('/getlimits').get(controller.getLimits);

this.app.route('/gettransaction').post(controller.getTransaction);
this.app.route('/broadcasttransaction').post(controller.broadcastTransaction);
Expand Down
7 changes: 6 additions & 1 deletion lib/api/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,16 @@ class Controller {
});
}

public getPairs = async (_req: Request, res: Response) => {
public getPairs = (_req: Request, res: Response) => {
const response = this.service.getPairs();
this.successResponse(res, response);
}

public getLimits = async (_req: Request, res: Response) => {
const response = this.service.getLimits();
this.successResponse(res, response);
}

public getTransaction = async (req: Request, res: Response) => {
try {
const { currency, transactionHash } = this.validateBody(req.body, [
Expand Down
15 changes: 8 additions & 7 deletions lib/notifications/NotificationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ type NotificationConfig = {

type CurrencyConfig = {
symbol: string;
channelbalance: number;
walletbalance: number;

maxSwapAmount: number;
minSwapAmount: number;

minWalletBalance: number;
minChannelBalance: number;
};

class NotificationProvider {
Expand Down Expand Up @@ -65,11 +69,8 @@ class NotificationProvider {
if (balance) {
const { channelBalance, walletBalance } = balance;

await this.checkBalance(currency.symbol, false, currency.channelbalance, channelBalance);

if (walletBalance) {
await this.checkBalance(currency.symbol, true, currency.walletbalance, walletBalance.totalBalance);
}
await this.checkBalance(currency.symbol, false, currency.minChannelBalance, channelBalance);
await this.checkBalance(currency.symbol, true, currency.minWalletBalance, walletBalance!.totalBalance);
}
}
}
Expand Down
73 changes: 68 additions & 5 deletions lib/rates/RateProvider.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import Logger from '../Logger';
import CryptoCompare from './CryptoCompare';
import { PairInstance } from 'lib/consts/Database';
import { getPairId, stringify, mapToObject, minutesToMilliseconds } from '../Utils';
import { PairInstance } from '../consts/Database';
import { CurrencyConfig } from '../notifications/NotificationProvider';
import { getPairId, stringify, mapToObject, minutesToMilliseconds, satoshisToWholeCoins, roundToDecimals } from '../Utils';

type Limits = {
minimal: number;
maximal: number;
};

class RateProvider {
// A map between pair ids and their rates
public rates = new Map<string, number>();

// A map between pair ids and their limits
public limits = new Map<string, Limits>();

// A map between quote and their base assets
private baseAssetsMap = new Map<string, string[]>();

// A map between assets and their limits
private currencies = new Map<string, Limits>();

private cryptoCompare = new CryptoCompare();

private timer!: NodeJS.Timeout;

constructor(private logger: Logger, private rateUpdateInterval: number) {
constructor(private logger: Logger, private rateUpdateInterval: number, currencies: CurrencyConfig[]) {
this.cryptoCompare = new CryptoCompare();

this.parseCurrencies(currencies);
}

/**
Expand All @@ -26,8 +40,16 @@ class RateProvider {
// If a pair has a hardcoded rate the CryptoCompare rate doesn't have to be queried
if (pair.rate) {
this.logger.debug(`Setting hardcoded rate for pair ${pair.id}: ${pair.rate}`);

this.rates.set(pair.id, pair.rate);

const limits = this.currencies.get(pair.base);

if (limits) {
this.logger.debug(`Setting limits for hardcoded pair ${pair.id}: ${stringify(limits)}`);
this.limits.set(pair.id, limits);
} else {
this.logger.warn(`Could not get limits for hardcoded pair ${pair.id}`);
}
return;
}

Expand Down Expand Up @@ -63,7 +85,11 @@ class RateProvider {
const baseAssetsRates = await this.cryptoCompare.getPriceMulti(baseAssets, [quoteAsset]);

baseAssets.forEach((baseAsset) => {
this.rates.set(getPairId({ base: baseAsset, quote: quoteAsset }), baseAssetsRates[baseAsset][quoteAsset]);
const pair = getPairId({ base: baseAsset, quote: quoteAsset });
const rate = baseAssetsRates[baseAsset][quoteAsset];

this.rates.set(pair, rate);
this.updateLimits(pair, baseAsset, quoteAsset, rate);
});

resolve();
Expand All @@ -73,8 +99,45 @@ class RateProvider {
await Promise.all(promises);

this.logger.debug(`Updated rates: ${stringify(mapToObject(this.rates))}`);
this.logger.debug(`Updated limits: ${stringify(mapToObject(this.limits))}`);
}

private updateLimits = (pair: string, base: string, quote: string, rate: number) => {
const baseLimits = this.currencies.get(base);
const quoteLimits = this.currencies.get(quote);

if (baseLimits && quoteLimits) {
// The limits we show are for the base asset and therefore to determine whether
// the limits for the base or quote asset are higher for minimal or lower for
// the maximal amount we need to multiply the quote limits times (1 / rate)
const reverseQuoteLimits = this.calculateQuoteLimits(rate, quoteLimits);

this.limits.set(pair, {
maximal: Math.min(baseLimits.maximal, reverseQuoteLimits.maximal),
minimal: Math.max(baseLimits.minimal, reverseQuoteLimits.minimal),
});
} else {
this.logger.warn(`Could not get limits for pair ${pair}`);
}
}

private calculateQuoteLimits = (rate: number, limits: Limits) => {
const reverseRate = 1 / rate;

return {
maximal: roundToDecimals(reverseRate * limits.maximal, 8),
minimal: roundToDecimals(reverseRate * limits.minimal, 8),
};
}

private parseCurrencies = (currencies: CurrencyConfig[]) => {
currencies.forEach((currency) => {
this.currencies.set(currency.symbol, {
maximal: satoshisToWholeCoins(currency.maxSwapAmount),
minimal: satoshisToWholeCoins(currency.minSwapAmount),
});
});
}
}

export default RateProvider;
8 changes: 8 additions & 0 deletions lib/service/Errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,12 @@ export default {
message: `order side not supported: ${orderSide}`,
code: concatErrorCode(ErrorCodePrefix.Service, 2),
}),
EXCEED_MAXIMAL_AMOUNT: (amount: number, maximalAmount: number) => ({
message: `${amount} is more than maximal ${maximalAmount}`,
code: concatErrorCode(ErrorCodePrefix.Service, 3),
}),
BENEATH_MINIMAL_AMOUNT: (amount: number, minimalAmount: number) => ({
message: `${amount} is less than minimal ${minimalAmount}`,
code: concatErrorCode(ErrorCodePrefix.Service, 4),
}),
};
50 changes: 46 additions & 4 deletions lib/service/Service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import bolt11 from '@boltz/bolt11';
import { EventEmitter } from 'events';
import Errors from './Errors';
import Logger from '../Logger';
Expand All @@ -7,8 +8,9 @@ import BoltzClient from '../boltz/BoltzClient';
import RateProvider from '../rates/RateProvider';
import { encodeBip21 } from './PaymentRequestUtils';
import { PairInstance, PairFactory } from '../consts/Database';
import { CurrencyConfig } from '../notifications/NotificationProvider';
import { OrderSide, OutputType, CurrencyInfo } from '../proto/boltzrpc_pb';
import { splitPairId, stringify, generateId, mapToObject } from '../Utils';
import { splitPairId, stringify, generateId, mapToObject, satoshisToWholeCoins } from '../Utils';

type PairConfig = {
base: string;
Expand Down Expand Up @@ -51,10 +53,16 @@ class Service extends EventEmitter {

private pairs = new Map<string, Pair>();

constructor(private logger: Logger, db: Database, private boltz: BoltzClient, rateInterval: number) {
constructor(
private logger: Logger,
private boltz: BoltzClient,
db: Database,
rateInterval: number,
currencies: CurrencyConfig[]) {

super();

this.rateProvider = new RateProvider(this.logger, rateInterval);
this.rateProvider = new RateProvider(this.logger, rateInterval, currencies);
this.pairRepository = new PairRepository(db.models);
}

Expand Down Expand Up @@ -142,7 +150,7 @@ class Service extends EventEmitter {
}
});

this.logger.verbose(`Initialised ${this.pairs.size} pairs: ${stringify(mapToObject(this.pairs))}`);
this.logger.verbose(`Initialised ${this.pairs.size} pairs: ${stringify(Array.from(this.pairs.keys()))}`);

const emitTransactionConfirmed = (id: string, transactionHash: string) =>
this.emit('swap.update', id, { message: `Transaction confirmed: ${transactionHash}` });
Expand Down Expand Up @@ -189,6 +197,13 @@ class Service extends EventEmitter {
return mapToObject(this.rateProvider.rates);
}

/**
* Gets the exchange limits for all supported pairs
*/
public getLimits = () => {
return mapToObject(this.rateProvider.limits);
}

/**
* Gets a hex encoded transaction from a transaction hash on the specified network
*/
Expand All @@ -214,6 +229,10 @@ class Service extends EventEmitter {
const chainCurrency = side === OrderSide.BUY ? quote : base;
const lightningCurrency = side === OrderSide.BUY ? base : quote;

const { millisatoshis } = bolt11.decode(invoice);

this.verifyAmount(millisatoshis / 1000, pairId, side, rate);

const {
address,
redeemScript,
Expand Down Expand Up @@ -252,6 +271,8 @@ class Service extends EventEmitter {

const side = this.getOrderSide(orderSide);

this.verifyAmount(amount, pairId, side, rate);

const {
invoice,
redeemScript,
Expand Down Expand Up @@ -299,6 +320,27 @@ class Service extends EventEmitter {
};
}

/**
* Verfies that the requested amount is neither above the maximal nor beneath the minimal
*/
private verifyAmount = (satoshis: number, pairId: string, orderSide: OrderSide, rate: number) => {
if (orderSide === OrderSide.SELL) {
// tslint:disable-next-line:no-parameter-reassignment
satoshis = satoshis * (1 / rate);
}

const limits = this.rateProvider.limits.get(pairId);

if (limits) {
const amount = satoshisToWholeCoins(satoshis);

if (amount > limits.maximal) throw Errors.EXCEED_MAXIMAL_AMOUNT(amount, limits.maximal);
if (amount < limits.minimal) throw Errors.BENEATH_MINIMAL_AMOUNT(amount, limits.minimal);
} else {
throw Errors.CURRENCY_NOT_SUPPORTED_BY_BACKEND(pairId);
}
}

/**
* Gets the corresponding OrderSide enum of a string
*/
Expand Down
Loading

0 comments on commit dd0dd76

Please sign in to comment.