Removing sponsors code.
Support new sponsor confirmation polling. fixes #319
This commit is contained in:
@@ -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();
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user