Removing sponsors code.

Support new sponsor confirmation polling.
fixes #319
This commit is contained in:
softsimon
2021-02-07 02:20:07 +07:00
parent d0edb3ff92
commit 9651fa7859
14 changed files with 107 additions and 430 deletions

View File

@@ -48,12 +48,5 @@
"BISQ_MARKETS": {
"ENABLED": false,
"DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
},
"SPONSORS": {
"ENABLED": false,
"BTCPAY_URL": "",
"BTCPAY_AUTH": "",
"BTCPAY_WEBHOOK_URL": "",
"TWITTER_BEARER_AUTH": ""
}
}

View File

@@ -1,198 +0,0 @@
import config from '../config';
import axios from 'axios';
import { DB } from '../database';
import logger from '../logger';
class Donations {
private notifyDonationStatusCallback: ((invoiceId: string) => void) | undefined;
private options = {
baseURL: config.SPONSORS.BTCPAY_URL,
headers: {
'Content-Type': 'application/json',
'Authorization': config.SPONSORS.BTCPAY_AUTH,
},
timeout: 10000,
};
sponsorsCache: any[] = [];
constructor() {}
public async $updateCache() {
try {
this.sponsorsCache = await this.$getDonationsFromDatabase('handle, image');
} catch (e) {
logger.warn('Setting sponsorsCache failed ' + e.message || e);
}
}
setNotfyDonationStatusCallback(fn: any): void {
this.notifyDonationStatusCallback = fn;
}
async $createRequest(amount: number, orderId: string): Promise<any> {
logger.notice('New invoice request. Handle: ' + orderId + ' Amount: ' + amount + ' BTC');
const postData = {
'price': amount,
'orderId': orderId,
'currency': 'BTC',
'itemDesc': 'Sponsor mempool.space',
'notificationUrl': config.SPONSORS.BTCPAY_WEBHOOK_URL,
'redirectURL': 'https://mempool.space/about',
};
const response = await axios.post('/invoices', postData, this.options);
return {
id: response.data.data.id,
amount: parseFloat(response.data.data.btcPrice),
addresses: response.data.data.addresses,
};
}
async $handleWebhookRequest(data: any): Promise<void> {
if (!data || !data.id) {
return;
}
const response = await this.$getStatus(data.id);
logger.notice(`Received BTCPayServer webhook. Invoice ID: ${data.id} Status: ${response.status} BTC Paid: ${response.btcPaid}`);
if (response.status !== 'complete' && response.status !== 'confirmed' && response.status !== 'paid') {
return;
}
if (this.notifyDonationStatusCallback) {
this.notifyDonationStatusCallback(data.id);
}
if (parseFloat(response.btcPaid) < 0.01) {
return;
}
if (response.orderId !== '') {
try {
const userData = await this.$getTwitterUserData(response.orderId);
const imageUrl = userData.profile_image_url.replace('normal', '200x200');
const imageBlob = await this.$downloadProfileImageBlob(imageUrl);
logger.debug('Creating database entry for donation with invoice id: ' + response.id);
await this.$addDonationToDatabase(response.btcPaid, userData.screen_name, userData.id, response.id, imageUrl, imageBlob);
this.$updateCache();
} catch (e) {
logger.err(`Error fetching twitter data for handle ${response.orderId}: ${e.message}`);
}
}
}
getSponsorImage(id: string): any | undefined {
const sponsor = this.sponsorsCache.find((s) => s.handle === id);
if (sponsor) {
return sponsor.image;
}
}
async $getDonationsFromDatabase(fields: string): Promise<any[]> {
try {
const connection = await DB.pool.getConnection();
const query = `SELECT ${fields} FROM donations ORDER BY id DESC`;
const [rows] = await connection.query<any>(query);
connection.release();
return rows;
} catch (e) {
logger.err('$getDonationsFromDatabase() error: ' + e.message || e);
return [];
}
}
private async $getOldDonations(): Promise<any[]> {
try {
const connection = await DB.pool.getConnection();
const query = `SELECT * FROM donations WHERE twitter_id IS NULL AND handle != ''`;
const [rows] = await connection.query<any>(query);
connection.release();
return rows;
} catch (e) {
logger.err('$getLegacyDonations() error' + e.message || e);
return [];
}
}
private async $getStatus(id: string): Promise<any> {
logger.debug('Fetching status for invoice: ' + id);
const response = await axios.get('/invoices/' + id, this.options);
logger.debug('Invoice status received: ' + JSON.stringify(response.data));
return response.data.data;
}
private async $addDonationToDatabase(btcPaid: number, handle: string, twitter_id: number | null,
orderId: string, imageUrl: string, image: string): Promise<void> {
try {
const connection = await DB.pool.getConnection();
const query = `INSERT IGNORE INTO donations(added, amount, handle, twitter_id, order_id, imageUrl, image) VALUES (NOW(), ?, ?, ?, ?, ?, FROM_BASE64(?))`;
const params: (string | number | null)[] = [
btcPaid,
handle,
twitter_id,
orderId,
imageUrl,
image,
];
const [result]: any = await connection.query(query, params);
connection.release();
} catch (e) {
logger.err('$addDonationToDatabase() error' + e.message || e);
}
}
private async $updateDonation(id: number, handle: string, twitterId: number, imageUrl: string, image: string): Promise<void> {
try {
const connection = await DB.pool.getConnection();
const query = `UPDATE donations SET handle = ?, twitter_id = ?, imageUrl = ?, image = FROM_BASE64(?) WHERE id = ?`;
const params: (string | number)[] = [
handle,
twitterId,
imageUrl,
image,
id,
];
const [result]: any = await connection.query(query, params);
connection.release();
} catch (e) {
logger.err('$updateDonation() error' + e.message || e);
}
}
private async $getTwitterUserData(handle: string): Promise<any> {
logger.debug('Fetching Twitter API data...');
const res = await axios.get(`https://api.twitter.com/1.1/users/show.json?screen_name=${handle}`, {
headers: {
Authorization: 'Bearer ' + config.SPONSORS.TWITTER_BEARER_AUTH
},
timeout: 10000,
});
logger.debug('Twitter user data fetched:' + JSON.stringify(res.data));
return res.data;
}
private async $downloadProfileImageBlob(url: string): Promise<string> {
logger.debug('Fetching image blob...');
const res = await axios.get(url, { responseType: 'arraybuffer', timeout: 10000 });
logger.debug('Image downloaded.');
return Buffer.from(res.data, 'utf8').toString('base64');
}
private async refreshSponsors(): Promise<void> {
const oldDonations = await this.$getOldDonations();
oldDonations.forEach(async (donation: any) => {
logger.debug('Migrating donation for handle: ' + donation.handle);
try {
const twitterData = await this.$getTwitterUserData(donation.handle);
const imageUrl = twitterData.profile_image_url.replace('normal', '200x200');
const imageBlob = await this.$downloadProfileImageBlob(imageUrl);
await this.$updateDonation(donation.id, twitterData.screen_name, twitterData.id, imageUrl, imageBlob);
} catch (e) {
logger.err('Failed to migrate donation for handle: ' + donation.handle + '. ' + (e.message || e));
}
});
}
}
export default new Donations();

View File

@@ -51,13 +51,6 @@ interface IConfig {
ENABLED: boolean;
DATA_PATH: string;
};
SPONSORS: {
ENABLED: boolean;
BTCPAY_URL: string;
BTCPAY_AUTH: string;
BTCPAY_WEBHOOK_URL: string;
TWITTER_BEARER_AUTH: string;
};
}
const defaults: IConfig = {
@@ -111,13 +104,6 @@ const defaults: IConfig = {
'ENABLED': false,
'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
},
'SPONSORS': {
'ENABLED': false,
'BTCPAY_URL': '',
'BTCPAY_AUTH': '',
'BTCPAY_WEBHOOK_URL': '',
'TWITTER_BEARER_AUTH': ''
}
};
class Config implements IConfig {
@@ -130,7 +116,6 @@ class Config implements IConfig {
STATISTICS: IConfig['STATISTICS'];
BISQ_BLOCKS: IConfig['BISQ_BLOCKS'];
BISQ_MARKETS: IConfig['BISQ_MARKETS'];
SPONSORS: IConfig['SPONSORS'];
constructor() {
const configs = this.merge(configFile, defaults);
@@ -143,7 +128,6 @@ class Config implements IConfig {
this.STATISTICS = configs.STATISTICS;
this.BISQ_BLOCKS = configs.BISQ_BLOCKS;
this.BISQ_MARKETS = configs.BISQ_MARKETS;
this.SPONSORS = configs.SPONSORS;
}
merge = (...objects: object[]): IConfig => {

View File

@@ -1,7 +1,6 @@
import { Express, Request, Response, NextFunction } from 'express';
import * as express from 'express';
import * as http from 'http';
import * as https from 'https';
import * as WebSocket from 'ws';
import * as cluster from 'cluster';
import axios from 'axios';
@@ -17,7 +16,6 @@ import websocketHandler from './api/websocket-handler';
import fiatConversion from './api/fiat-conversion';
import bisq from './api/bisq/bisq';
import bisqMarkets from './api/bisq/markets';
import donations from './api/donations';
import logger from './logger';
import backendInfo from './api/backend-info';
import loadingIndicators from './api/loading-indicators';
@@ -25,7 +23,7 @@ import mempool from './api/mempool';
class Server {
private wss: WebSocket.Server | undefined;
private server: https.Server | http.Server | undefined;
private server: http.Server | undefined;
private app: Express;
private currentBackendRetryInterval = 5;
@@ -87,10 +85,6 @@ class Server {
fiatConversion.startService();
if (config.SPONSORS.ENABLED) {
donations.$updateCache();
}
this.setUpHttpApiRoutes();
this.setUpWebsocketHandling();
this.runMainUpdateLoop();
@@ -144,7 +138,6 @@ class Server {
statistics.setNewStatisticsEntryCallback(websocketHandler.handleNewStatistic.bind(websocketHandler));
blocks.setNewBlockCallback(websocketHandler.handleNewBlock.bind(websocketHandler));
memPool.setMempoolChangedCallback(websocketHandler.handleMempoolChange.bind(websocketHandler));
donations.setNotfyDonationStatusCallback(websocketHandler.handleNewDonation.bind(websocketHandler));
fiatConversion.setProgressChangedCallback(websocketHandler.handleNewConversionRates.bind(websocketHandler));
loadingIndicators.setProgressChangedCallback(websocketHandler.handleLoadingChanged.bind(websocketHandler));
}
@@ -156,6 +149,24 @@ class Server {
.get(config.MEMPOOL.API_URL_PREFIX + 'fees/mempool-blocks', routes.getMempoolBlocks)
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', routes.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', routes.getInitData)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get('http://localhost:9000/api/v1/donations', { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get('http://localhost:9000/api/v1/donations/images/' + req.params.id, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
;
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED) {
@@ -195,35 +206,6 @@ class Server {
;
}
if (config.SPONSORS.ENABLED) {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', routes.getDonations.bind(routes))
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', routes.getSponsorImage.bind(routes))
.post(config.MEMPOOL.API_URL_PREFIX + 'donations', routes.createDonationRequest.bind(routes))
.post(config.MEMPOOL.API_URL_PREFIX + 'donations-webhook', routes.donationWebhook.bind(routes))
;
} else {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get('https://mempool.space/api/v1/donations/images/' + req.params.id, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
});
}
if (config.MEMPOOL.BACKEND !== 'esplora') {
this.app
.get(config.MEMPOOL.API_URL_PREFIX + 'mempool', routes.getMempool)

View File

@@ -8,10 +8,9 @@ import mempool from './api/mempool';
import bisq from './api/bisq/bisq';
import websocketHandler from './api/websocket-handler';
import bisqMarket from './api/bisq/markets-api';
import { OptimizedStatistic, RequiredSpec, TransactionExtended } from './mempool.interfaces';
import { RequiredSpec, TransactionExtended } from './mempool.interfaces';
import { MarketsApiError } from './api/bisq/interfaces';
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
import donations from './api/donations';
import logger from './logger';
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
import transactionUtils from './api/transaction-utils';
@@ -99,79 +98,6 @@ class Routes {
res.json(backendInfo.getBackendInfo());
}
public async createDonationRequest(req: Request, res: Response) {
const constraints: RequiredSpec = {
'amount': {
required: true,
types: ['@float']
},
'orderId': {
required: true,
types: ['@string']
}
};
const p = this.parseRequestParameters(req.body, constraints);
if (p.error) {
res.status(400).send(p.error);
return;
}
if (p.orderId !== '' && !/^(@|)[a-zA-Z0-9_]{1,15}$/.test(p.orderId)) {
res.status(400).send('Invalid Twitter handle');
return;
}
if (p.amount < 0.001) {
res.status(400).send('Amount needs to be at least 0.001');
return;
}
if (p.amount > 1000) {
res.status(400).send('Amount too large');
return;
}
try {
const result = await donations.$createRequest(p.amount, p.orderId);
res.json(result);
} catch (e) {
res.status(500).send(e.message);
}
}
public async getDonations(req: Request, res: Response) {
try {
const result = await donations.$getDonationsFromDatabase('handle, imageUrl');
res.json(result);
} catch (e) {
res.status(500).send(e.message);
}
}
public async getSponsorImage(req: Request, res: Response) {
try {
const result = await donations.getSponsorImage(req.params.id);
if (result) {
res.set('Content-Type', 'image/jpeg');
res.send(result);
} else {
res.status(404).end();
}
} catch (e) {
res.status(500).send(e.message);
}
}
public async donationWebhook(req: Request, res: Response) {
try {
donations.$handleWebhookRequest(req.body);
res.end();
} catch (e) {
res.status(500).send(e);
}
}
public getBisqStats(req: Request, res: Response) {
const result = bisq.getStats();
res.json(result);