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

Commit

Permalink
feat: monitor balances (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 authored Jan 28, 2019
1 parent 2a6fdcb commit c9d9278
Show file tree
Hide file tree
Showing 15 changed files with 766 additions and 23 deletions.
22 changes: 21 additions & 1 deletion bin/boltzm
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const { argv } = require('yargs').options({
type: 'number',
},
'api.interval': {
descibe: 'The interval at which your rates should be updated in minutes',
descibe: 'Interval at which your rates should be updated in minutes',
type: 'string',
},
'boltz.host': {
Expand All @@ -49,6 +49,26 @@ const { argv } = require('yargs').options({
describe: 'Path to the SSL certificate of Boltz',
type: 'string',
},
'notification.name': {
describe: 'The name of this boltz instance',
type: 'string',
},
'notification.interval': {
describe: 'Interval at which the balances of the backend should be checked in minutes',
type: 'string',
},
'notification.token': {
describe: 'Slack Bot user OAuth access token for the notification bot',
type: 'string',
},
'notification.channel': {
describe: 'Name of the Slack channel to which notifications should be sent',
type: 'string',
},
currencies: {
describe: 'Currencies that should be monitored by the middleware',
type: 'string',
},
pairs: {
describe: 'Pairs that should be offered by the middleware',
type: 'array',
Expand Down
17 changes: 14 additions & 3 deletions lib/BoltzMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Arguments } from 'yargs';
import Api from './api/Api';
import Config from './Config';
import Logger from './Logger';
import BoltzClient from './boltz/BoltzClient';
import Api from './api/Api';
import Service from './service/Service';
import Database from './db/Database';
import Service from './service/Service';
import BoltzClient from './boltz/BoltzClient';
import NotificationProvider from './notifications/NotificationProvider';

class BoltzMiddleware {
private config: Config;
Expand All @@ -14,6 +15,7 @@ class BoltzMiddleware {
private boltzClient: BoltzClient;

private service: Service;
private notifications: NotificationProvider;

private api: Api;

Expand All @@ -27,6 +29,14 @@ class BoltzMiddleware {
this.boltzClient = new BoltzClient(this.logger, this.config.boltz);

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

this.notifications = new NotificationProvider(
this.logger,
this.boltzClient,
this.config.notification,
this.config.currencies,
);

this.api = new Api(this.logger, this.config.api, this.service);
}

Expand All @@ -37,6 +47,7 @@ class BoltzMiddleware {
]);

await this.service.init(this.config.pairs);
await this.notifications.init();

this.api.init();
}
Expand Down
40 changes: 38 additions & 2 deletions lib/Config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Arguments } from 'yargs';
import fs from 'fs';
import path from 'path';
import toml from 'toml';
import { Arguments } from 'yargs';
import { ApiConfig } from './api/Api';
import { PairConfig } from './service/Service';
import { BoltzConfig } from './boltz/BoltzClient';
import { getServiceDir, deepMerge, resolveHome } from './Utils';
import { PairConfig } from './service/Service';
import { NotificationConfig, CurrencyConfig } from './notifications/NotificationProvider';

class Config {
public logpath: string;
Expand All @@ -17,6 +18,10 @@ class Config {

public boltz: BoltzConfig;

public notification: NotificationConfig;

public currencies: CurrencyConfig[];

public pairs: PairConfig[];

private defaultDataDir = getServiceDir('boltz-middleware');
Expand Down Expand Up @@ -47,11 +52,42 @@ class Config {
certpath: path.join(getServiceDir('boltz'), 'tls.cert'),
};

this.notification = {
name: '',
interval: 1,

token: '',
channel: '',
};

this.currencies = [
{
symbol: 'BTC',
walletbalance: 1000000,
channelbalance: 500000,
},
{
symbol: 'LTC',
walletbalance: 100000000,
channelbalance: 50000000,
},
];

this.pairs = [
{
base: 'LTC',
quote: 'BTC',
},
{
base: 'BTC',
quote: 'BTC',
rate: 1,
},
{
base: 'LTC',
quote: 'LTC',
rate: 1,
},
];
}

Expand Down
14 changes: 14 additions & 0 deletions lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,17 @@ export const resolveHome = (filename: string) => {

return filename;
};

/**
* Convert minutes into milliseconds
*/
export const minutesToMilliseconds = (minutes: number) => {
return minutes * 60 * 1000;
};

/**
* Convert satoshis to whole coins and remove trailing zeros
*/
export const satoshisToWholeCoins = (satoshis: number) => {
return Number((satoshis / 100000000).toFixed(8));
};
28 changes: 28 additions & 0 deletions lib/boltz/BoltzClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,34 @@ class BoltzClient extends BaseClient {
return this.unaryCall<boltzrpc.GetInfoRequest, boltzrpc.GetInfoResponse.AsObject>('getInfo', new boltzrpc.GetInfoRequest());
}

/**
* Gets the balance for either all wallets or just a single one if specified
*/
public getBalance = (currency?: string) => {
const request = new boltzrpc.GetBalanceRequest();

if (currency) {
request.setCurrency(currency);
}

return this.unaryCall<boltzrpc.GetBalanceRequest, boltzrpc.GetBalanceResponse.AsObject>('getBalance', request);
}

/**
* Gets a new address of a specified wallet. The "type" parameter is optional and defaults to "OutputType.LEGACY"
*/
public newAddress = (currency: string, outputType?: boltzrpc.OutputType) => {
const request = new boltzrpc.NewAddressRequest();

request.setCurrency(currency);

if (outputType) {
request.setType(outputType);
}

return this.unaryCall<boltzrpc.NewAddressRequest, boltzrpc.NewAddressResponse.AsObject>('newAddress', request);
}

/**
* Gets a hex encoded transaction from a transaction hash on the specified network
*/
Expand Down
151 changes: 151 additions & 0 deletions lib/notifications/NotificationProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import Logger from '../Logger';
import SlackClient from './SlackClient';
import BoltzClient from '../boltz/BoltzClient';
import { Balance, OutputType } from '../proto/boltzrpc_pb';
import { minutesToMilliseconds, satoshisToWholeCoins } from '../Utils';

type NotificationConfig = {
name: string;
interval: number;

token: string;
channel: string;
};

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

class NotificationProvider {
private slack: SlackClient;
private timer!: NodeJS.Timer;

// These Sets contains the symbols for which an alert notification was sent
private walletAlerts = new Set<string>();
private channelAlerts = new Set<string>();

constructor(
private logger: Logger,
private boltz: BoltzClient,
private config: NotificationConfig,
private currencies: CurrencyConfig[]) {

this.slack = new SlackClient(config.token, config.channel, config.name);
}

public init = async () => {
try {
await this.slack.sendMessage('Started Boltz instance');
this.logger.verbose('Connected to Slack');

await this.checkBalances();

this.logger.silly(`Checking balances every ${this.config.interval} minutes`);

this.timer = setInterval(async () => {
await this.checkBalances();
}, minutesToMilliseconds(this.config.interval));
} catch (error) {
this.logger.warn(`Could not connect to Slack: ${error}`);
}
}

public disconnect = () => {
clearInterval(this.timer);
}

private checkBalances = async () => {
const balances = await this.parseBalances();

for (const currency of this.currencies) {
const balance = balances.get(currency.symbol);

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);
}
}
}
}

private checkBalance = async (currency: string, isWallet: boolean, expectedBalance: number, actualBalance: number) => {
const set = isWallet ? this.walletAlerts : this.channelAlerts;
const sentAlert = set.has(currency);

if (sentAlert) {
if (actualBalance > expectedBalance) {
set.delete(currency);
await this.sendRelief(currency, isWallet, expectedBalance, actualBalance);
}
} else {
if (actualBalance < expectedBalance) {
set.add(currency);
await this.sendAlert(currency, isWallet, expectedBalance, actualBalance);
}
}
}

private sendAlert = async (currency: string, isWallet: boolean, expectedBalance: number, actualBalance: number) => {
const { expected, actual } = this.formatBalances(expectedBalance, actualBalance);
const missing = satoshisToWholeCoins(expectedBalance - actualBalance);

const { address } = await this.boltz.newAddress(currency, OutputType.COMPATIBILITY);

const walletName = this.getWalletName(isWallet);

this.logger.warn(`${currency} ${walletName} balance is less than ${expectedBalance}: ${actualBalance}`);

// tslint:disable-next-line:prefer-template
let slackMessage = ':rotating_light: *Alert* :rotating_light:\n\n' +
`The ${currency} ${walletName} balance of ${actual} ${currency} is less than expected ${expected} ${currency}\n\n` +
`Funds missing: *${missing} ${currency}*`;

if (isWallet) {
slackMessage += `\nDeposit address: *${address}*`;
}

await this.slack.sendMessage(slackMessage);
}

private sendRelief = async (currency: string, isWallet: boolean, expectedBalance: number, actualBalance: number) => {
const { expected, actual } = this.formatBalances(expectedBalance, actualBalance);
const walletName = this.getWalletName(isWallet);

this.logger.info(`${currency} ${walletName} balance is more than expected ${expectedBalance} again: ${actualBalance}`);

await this.slack.sendMessage(
`The ${currency} ${walletName} balance of ${actual} ${currency} is more than expected ${expected} ${currency} again`,
);
}

private formatBalances = (expectedBalance: number, actualBalance: number) => {
return {
expected: satoshisToWholeCoins(expectedBalance),
actual: satoshisToWholeCoins(actualBalance),
};
}

private getWalletName = (isWallet: boolean) => {
return isWallet ? 'wallet' : 'channel';
}

private parseBalances = async () => {
const balance = await this.boltz.getBalance();
const balances = new Map<string, Balance.AsObject>();

balance.balancesMap.forEach((balance) => {
balances.set(balance[0], balance[1]);
});

return balances;
}
}

export default NotificationProvider;
export { NotificationConfig, CurrencyConfig };
18 changes: 18 additions & 0 deletions lib/notifications/SlackClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { WebClient } from '@slack/client';

class SlackClient {
private client: WebClient;

constructor(token: string, private channel: string, private name: string) {
this.client = new WebClient(token);
}

public sendMessage = async (message: string) => {
await this.client.chat.postMessage({
channel: this.channel,
text: `[${this.name}]: ${message}`,
});
}
}

export default SlackClient;
Loading

0 comments on commit c9d9278

Please sign in to comment.