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

Commit

Permalink
feat: scheduled database backups
Browse files Browse the repository at this point in the history
  • Loading branch information
michael1011 committed Apr 20, 2019
1 parent 8195604 commit 1dba6dd
Show file tree
Hide file tree
Showing 11 changed files with 921 additions and 95 deletions.
4 changes: 2 additions & 2 deletions bin/report
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
const { generateReport } = require('../dist/report/Report');
const { reportCli } = require('../dist/report/Report');

const { argv } = require('yargs').options({
dbpath: {
Expand All @@ -18,4 +18,4 @@ delete argv.$0;
delete argv.help;
delete argv.version;

generateReport(argv);
reportCli(argv);
23 changes: 19 additions & 4 deletions lib/BoltzMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Logger from './Logger';
import Database from './db/Database';
import Service from './service/Service';
import BoltzClient from './boltz/BoltzClient';
import BackupScheduler from './backup/BackupScheduler';
import NotificationProvider from './notifications/NotificationProvider';

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

private service: Service;
private backup: BackupScheduler;
private notifications: NotificationProvider;

private api: Api;
Expand All @@ -35,10 +37,19 @@ class BoltzMiddleware {
this.config.currencies,
);

this.backup = new BackupScheduler(
this.logger,
this.config.dbpath,
this.config.backup,
this.service.swapRepository,
this.service.reverseSwapRepository,
);

this.notifications = new NotificationProvider(
this.logger,
this.service,
this.boltzClient,
this.backup,
this.config.notification,
this.config.currencies,
);
Expand All @@ -47,11 +58,15 @@ class BoltzMiddleware {
}

public start = async () => {
await this.db.init(),
await this.boltzClient.connect(),
await Promise.all([
this.db.init(),
this.boltzClient.connect(),
]);

await this.service.init(this.config.pairs);
await this.notifications.init();
await Promise.all([
this.service.init(this.config.pairs),
this.notifications.init(),
]);

await this.api.init();
}
Expand Down
19 changes: 18 additions & 1 deletion lib/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ApiConfig } from './api/Api';
import { PairConfig } from './service/Service';
import { CurrencyConfig } from './consts/Types';
import { BoltzConfig } from './boltz/BoltzClient';
import { BackupConfig } from './backup/BackupScheduler';
import { getServiceDir, deepMerge, resolveHome } from './Utils';
import { NotificationConfig } from './notifications/NotificationProvider';

Expand All @@ -20,6 +21,7 @@ class Config {
public boltz: BoltzConfig;

public notification: NotificationConfig;
public backup: BackupConfig;

public currencies: CurrencyConfig[];

Expand All @@ -31,7 +33,7 @@ class Config {
private configpath: string;

constructor() {
const { configpath, logpath, dbpath } = this.getDataDirPaths(this.defaultDataDir);
const { configpath, logpath, dbpath, backup } = this.getDataDirPaths(this.defaultDataDir);

this.configpath = configpath;

Expand Down Expand Up @@ -61,6 +63,17 @@ class Config {
interval: 1,
};

this.backup = {
email: '',
privatekeypath: backup.privatekeypath,

bucketname: '',

interval: '0 0 * * *',

backenddbpath: '',
};

this.currencies = [
{
symbol: 'BTC',
Expand Down Expand Up @@ -123,6 +136,7 @@ class Config {
'logpath',
'dbpath',
'boltz.certpath',
'backup.privatekeypath',
];

pathsToResolve.forEach((key) => {
Expand Down Expand Up @@ -165,6 +179,9 @@ class Config {
configpath: path.join(dataDir, 'boltz.conf'),
logpath: path.join(dataDir, 'boltz.log'),
dbpath: path.join(dataDir, 'boltz.db'),
backup: {
privatekeypath: path.join(dataDir, 'backupPrivatekey.pem'),
},
};
}
}
Expand Down
111 changes: 111 additions & 0 deletions lib/backup/BackupScheduler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { readFileSync } from 'fs';
import { scheduleJob } from 'node-schedule';
import { Storage, Bucket } from '@google-cloud/storage';
import Logger from '../Logger';
import { generateReport } from '../report/Report';
import SwapRepository from '../service/SwapRepository';
import ReverseSwapRepository from '../service/ReverseSwapRepository';

type BackupConfig = {
email: string;
privatekeypath: string;

bucketname: string;

// The interval has to be a cron schedule expression
interval: string;

// If set, the database of the backend will also be backed up
backenddbpath: string;
};

class BackupScheduler {
private bucket?: Bucket;

constructor(
private logger: Logger,
private dbpath: string,
private config: BackupConfig,
private swapRepository: SwapRepository,
private reverseSwapRepository: ReverseSwapRepository) {

if (
config.email === '' ||
config.privatekeypath === '' ||
config.bucketname === ''
) {
logger.warn('Disabled backups because of incomplete configuration');
return;
}

const storage = new Storage({
credentials: {
client_email: config.email,
private_key: readFileSync(config.privatekeypath, 'utf-8'),
},
});

this.bucket = storage.bucket(config.bucketname);

scheduleJob(this.config.interval, async (date) => {
await this.uploadDatabases(date);
await this.uploadReport();
});
}

public uploadDatabases = async (date: Date) => {
if (!this.bucket) {
throw 'Backups are disabled because of incomplete configuration';
}

const dateString = this.getDate(date);
this.logger.silly(`Doing database backup at: ${dateString}`);

await this.uploadFile(this.dbpath, dateString, true);

if (this.config.backenddbpath !== '') {
await this.uploadFile(this.config.backenddbpath, dateString, false);
}
}

public uploadReport = async () => {
if (!this.bucket) {
return;
}

const file = this.bucket.file('report.csv');
const data = await generateReport(this.swapRepository, this.reverseSwapRepository);

await file.save(data);

this.logger.debug('Uploaded report');
}

private uploadFile = async (fileName: string, date: string, isMiddleware: boolean) => {
try {
await this.bucket!.upload(fileName, {
destination: `${isMiddleware ? 'middleware' : 'backend'}/database-${date}.db`,
});

this.logger.debug(`Uploaded file ${fileName}`);
} catch (error) {
this.logger.warn(`Could not upload file: ${error}`);
throw error;
}
}

private getDate = (date: Date) => {
return `${date.getFullYear()}${this.addLeadingZeros(date.getMonth())}${this.addLeadingZeros(date.getDay())}` +
`-${this.addLeadingZeros(date.getHours())}${this.addLeadingZeros(date.getMinutes())}`;
}

/**
* Adds a leading 0 to the provided number if it is smalled than 10
*/
private addLeadingZeros = (number: number) => {
return `${number}`.padStart(2, '0');
}
}

export default BackupScheduler;
export { BackupConfig };
71 changes: 31 additions & 40 deletions lib/notifications/CommandHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,20 @@ import BoltzClient from '../boltz/BoltzClient';
import { OutputType } from '../proto/boltzrpc_pb';
import Swap from '../db/models/Swap';
import ReverseSwap from '../db/models/ReverseSwap';
import { SwapUpdateEvent } from '../consts/Enums';
import SwapRepository from '../service/SwapRepository';
import ReverseSwapRepository from '../service/ReverseSwapRepository';
import { getSuccessfulTrades } from '../report/Report';
import BackupScheduler from '../backup/BackupScheduler';
import { satoshisToCoins, parseBalances, getFeeSymbol, stringify } from '../Utils';

/**
* Gets all successful (reverse) swaps
*/
export const getSuccessfulTrades = async (swapRepository: SwapRepository, reverseSwapRepository: ReverseSwapRepository):
Promise<{ swaps: Swap[], reverseSwaps: ReverseSwap[] }> => {

const [swaps, reverseSwaps] = await Promise.all([
swapRepository.getSwaps({
status: {
[Op.eq]: SwapUpdateEvent.InvoicePaid,
},
}),
reverseSwapRepository.getReverseSwaps({
status: {
[Op.eq]: SwapUpdateEvent.InvoiceSettled,
},
}),
]);

return {
swaps,
reverseSwaps,
};
};

enum Command {
Help = 'help',

// Commands that retrieve information
GetFees = 'getfees',
SwapInfo = 'swapinfo',
GetBalance = 'getbalance',

// Commands that generate a value or trigger a function
Backup = 'backup',
NewAddress = 'newaddress',
ToggleReverseSwaps = 'togglereverse',
}
Expand All @@ -56,17 +34,20 @@ class CommandHandler {

constructor(
private logger: Logger,
private discord: DiscordClient,
private service: Service,
private boltz: BoltzClient,
private discord: DiscordClient) {
private backupScheduler: BackupScheduler) {

this.commands = new Map<string, CommandInfo>([
[Command.Help, { description: 'gets a list of all available commands', executor: this.help }],

[Command.GetFees, { description: 'gets the accumulated fees', executor: this.getFees }],
[Command.NewAddress, { description: 'generates a new address for a currency', executor: this.newAddress }],
[Command.GetBalance, { description: 'gets the balance of the wallet and channels', executor: this.getBalance }],
[Command.SwapInfo, { description: 'gets all available information about a (reverse) swap', executor: this.swapInfo }],

[Command.Backup, { description: 'uploads a backup of the databases', executor: this.backup }],
[Command.NewAddress, { description: 'generates a new address for a currency', executor: this.newAddress }],
[Command.ToggleReverseSwaps, { description: 'enables or disables reverse swaps', executor: this.toggleReverseSwaps }],
]);

Expand All @@ -91,10 +72,20 @@ class CommandHandler {
});
}

/**
/*
* Command executors
*/

private help = async () => {
let message = 'Commands:\n';

this.commands.forEach((info, command) => {
message += `\n**${command}**: ${info.description}`;
});

await this.discord.sendMessage(message);
}

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

Expand Down Expand Up @@ -191,17 +182,17 @@ class CommandHandler {
await this.discord.sendMessage(`${this.service.allowReverseSwaps ? 'Enabled' : 'Disabled'} reverse swaps`);
}

private help = async () => {
let message = 'Commands:\n';

this.commands.forEach((info, command) => {
message += `\n**${command}**: ${info.description}`;
});
private backup = async () => {
try {
await this.backupScheduler.uploadDatabases(new Date());

await this.discord.sendMessage(message);
await this.discord.sendMessage('Uploaded backup of databases');
} catch (error) {
await this.discord.sendMessage(`Could not upload backup: ${error}`);
}
}

/**
/*
* Helper functions
*/

Expand All @@ -220,7 +211,7 @@ class CommandHandler {
const fees = new Map<string, number>();

const getFeeFromSwapMap = (array: Swap[] | ReverseSwap[], isReverse: boolean) => {
array.forEach((swap) => {
array.forEach((swap: Swap | ReverseSwap) => {
const feeSymbol = getFeeSymbol(swap.pair, swap.orderSide, isReverse);
const fee = fees.get(feeSymbol);

Expand Down
5 changes: 4 additions & 1 deletion lib/notifications/NotificationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { OutputType, OrderSide } from '../proto/boltzrpc_pb';
import BoltzClient, { ConnectionStatus } from '../boltz/BoltzClient';
import Swap from '../db/models/Swap';
import ReverseSwap from '../db/models/ReverseSwap';
import BackupScheduler from '../backup/BackupScheduler';
import { minutesToMilliseconds, satoshisToCoins, splitPairId, parseBalances, getFeeSymbol } from '../Utils';

type NotificationConfig = {
Expand Down Expand Up @@ -36,6 +37,7 @@ class NotificationProvider {
private logger: Logger,
private service: Service,
private boltz: BoltzClient,
private backup: BackupScheduler,
private config: NotificationConfig,
private currencies: CurrencyConfig[]) {

Expand All @@ -51,9 +53,10 @@ class NotificationProvider {

new CommandHandler(
this.logger,
this.discord,
this.service,
this.boltz,
this.discord,
this.backup,
);
}

Expand Down
Loading

0 comments on commit 1dba6dd

Please sign in to comment.