2020-10-07 20:15:42 +07:00
|
|
|
const config = require('../../mempool-config.json');
|
|
|
|
import * as request from 'request';
|
|
|
|
import { DB } from '../database';
|
2020-10-13 15:27:52 +07:00
|
|
|
import logger from '../logger';
|
2020-10-07 20:15:42 +07:00
|
|
|
|
|
|
|
class Donations {
|
|
|
|
private notifyDonationStatusCallback: ((invoiceId: string) => void) | undefined;
|
|
|
|
private options = {
|
|
|
|
baseUrl: config.BTCPAY_URL,
|
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
'Authorization': config.BTCPAY_AUTH,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2020-10-18 17:14:35 +07:00
|
|
|
sponsorsCache: any[] = [];
|
|
|
|
|
2020-10-16 16:29:54 +07:00
|
|
|
constructor() {
|
2020-10-18 17:14:35 +07:00
|
|
|
this.$updateCache();
|
|
|
|
}
|
|
|
|
|
|
|
|
async $updateCache() {
|
|
|
|
try {
|
|
|
|
this.sponsorsCache = await this.$getDonationsFromDatabase('handle, image');
|
|
|
|
} catch (e) {
|
|
|
|
logger.warn('Setting sponsorsCache failed ' + e.message || e);
|
|
|
|
}
|
2020-10-16 16:29:54 +07:00
|
|
|
}
|
2020-10-07 20:15:42 +07:00
|
|
|
|
2020-10-17 15:01:58 +07:00
|
|
|
setNotfyDonationStatusCallback(fn: any): void {
|
2020-10-07 20:15:42 +07:00
|
|
|
this.notifyDonationStatusCallback = fn;
|
|
|
|
}
|
|
|
|
|
|
|
|
createRequest(amount: number, orderId: string): Promise<any> {
|
2020-10-13 18:26:10 +07:00
|
|
|
logger.notice('New invoice request. Handle: ' + orderId + ' Amount: ' + amount + ' BTC');
|
|
|
|
|
2020-10-07 20:15:42 +07:00
|
|
|
const postData = {
|
|
|
|
'price': amount,
|
|
|
|
'orderId': orderId,
|
|
|
|
'currency': 'BTC',
|
|
|
|
'itemDesc': 'Sponsor mempool.space',
|
2020-10-08 00:15:26 +07:00
|
|
|
'notificationUrl': config.BTCPAY_WEBHOOK_URL,
|
2020-10-07 20:15:42 +07:00
|
|
|
'redirectURL': 'https://mempool.space/about'
|
|
|
|
};
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
request.post({
|
|
|
|
uri: '/invoices',
|
|
|
|
json: postData,
|
|
|
|
...this.options,
|
|
|
|
}, (err, res, body) => {
|
|
|
|
if (err) { return reject(err); }
|
|
|
|
const formattedBody = {
|
|
|
|
id: body.data.id,
|
|
|
|
amount: parseFloat(body.data.btcPrice),
|
|
|
|
address: body.data.bitcoinAddress,
|
|
|
|
};
|
|
|
|
resolve(formattedBody);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-17 15:01:58 +07:00
|
|
|
async $handleWebhookRequest(data: any): Promise<void> {
|
2020-10-07 20:15:42 +07:00
|
|
|
if (!data || !data.id) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const response = await this.getStatus(data.id);
|
2020-10-13 18:26:10 +07:00
|
|
|
logger.notice(`Received BTCPayServer webhook. Invoice ID: ${data.id} Status: ${response.status} BTC Paid: ${response.btcPaid}`);
|
2020-10-08 04:46:11 +09:00
|
|
|
if (response.status !== 'complete' && response.status !== 'confirmed' && response.status !== 'paid') {
|
2020-10-08 00:20:42 +07:00
|
|
|
return;
|
|
|
|
}
|
2020-10-07 20:15:42 +07:00
|
|
|
|
2020-10-08 00:20:42 +07:00
|
|
|
if (this.notifyDonationStatusCallback) {
|
|
|
|
this.notifyDonationStatusCallback(data.id);
|
|
|
|
}
|
2020-10-07 20:15:42 +07:00
|
|
|
|
2020-10-13 19:54:47 +07:00
|
|
|
if (parseFloat(response.btcPaid) < 0.01) {
|
2020-10-08 00:20:42 +07:00
|
|
|
return;
|
2020-10-07 20:15:42 +07:00
|
|
|
}
|
2020-10-08 00:20:42 +07:00
|
|
|
|
|
|
|
if (response.orderId !== '') {
|
|
|
|
try {
|
2020-10-16 16:29:54 +07:00
|
|
|
const userData = await this.$getTwitterUserData(response.orderId);
|
2020-10-17 15:01:58 +07:00
|
|
|
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);
|
2020-10-18 17:14:35 +07:00
|
|
|
await this.$addDonationToDatabase(response.btcPaid, userData.screen_name, userData.id, response.id, imageUrl, imageBlob);
|
|
|
|
this.$updateCache();
|
2020-10-08 00:20:42 +07:00
|
|
|
} catch (e) {
|
2020-10-17 15:01:58 +07:00
|
|
|
logger.err(`Error fetching twitter data for handle ${response.orderId}: ${e.message}`);
|
2020-10-08 00:20:42 +07:00
|
|
|
}
|
|
|
|
}
|
2020-10-16 16:29:54 +07:00
|
|
|
}
|
|
|
|
|
2020-10-18 17:14:35 +07:00
|
|
|
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[]> {
|
2020-10-16 16:29:54 +07:00
|
|
|
try {
|
|
|
|
const connection = await DB.pool.getConnection();
|
2020-10-18 17:14:35 +07:00
|
|
|
const query = `SELECT ${fields} FROM donations ORDER BY id DESC`;
|
2020-10-16 16:29:54 +07:00
|
|
|
const [rows] = await connection.query<any>(query);
|
|
|
|
connection.release();
|
|
|
|
return rows;
|
|
|
|
} catch (e) {
|
|
|
|
logger.err('$getDonationsFromDatabase() error' + e);
|
2020-10-17 15:01:58 +07:00
|
|
|
return [];
|
2020-10-16 16:29:54 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-17 15:01:58 +07:00
|
|
|
private async $getOldDonations(): Promise<any[]> {
|
2020-10-16 16:29:54 +07:00
|
|
|
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);
|
2020-10-17 15:01:58 +07:00
|
|
|
return [];
|
2020-10-16 16:29:54 +07:00
|
|
|
}
|
2020-10-07 20:15:42 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
private getStatus(id: string): Promise<any> {
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-10-13 15:27:52 +07:00
|
|
|
logger.debug('Fetching status for invoice: ' + id);
|
2020-10-07 20:15:42 +07:00
|
|
|
request.get({
|
|
|
|
uri: '/invoices/' + id,
|
|
|
|
json: true,
|
|
|
|
...this.options,
|
|
|
|
}, (err, res, body) => {
|
|
|
|
if (err) { return reject(err); }
|
2020-10-13 15:27:52 +07:00
|
|
|
logger.debug('Invoice status received: ' + JSON.stringify(body.data));
|
2020-10-07 20:15:42 +07:00
|
|
|
resolve(body.data);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-16 16:29:54 +07:00
|
|
|
private async $addDonationToDatabase(btcPaid: number, handle: string, twitter_id: number | null,
|
|
|
|
orderId: string, imageUrl: string, image: string): Promise<void> {
|
2020-10-07 20:15:42 +07:00
|
|
|
try {
|
|
|
|
const connection = await DB.pool.getConnection();
|
2020-10-16 16:29:54 +07:00
|
|
|
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);
|
2020-10-07 20:15:42 +07:00
|
|
|
connection.release();
|
|
|
|
} catch (e) {
|
2020-10-16 16:29:54 +07:00
|
|
|
logger.err('$addDonationToDatabase() error' + e);
|
2020-10-07 20:15:42 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-16 16:29:54 +07:00
|
|
|
private async $updateDonation(id: number, handle: string, twitterId: number, imageUrl: string, image: string): Promise<void> {
|
2020-10-07 20:15:42 +07:00
|
|
|
try {
|
|
|
|
const connection = await DB.pool.getConnection();
|
2020-10-16 16:29:54 +07:00
|
|
|
const query = `UPDATE donations SET handle = ?, twitter_id = ?, imageUrl = ?, image = FROM_BASE64(?) WHERE id = ?`;
|
2020-10-07 20:15:42 +07:00
|
|
|
const params: (string | number)[] = [
|
2020-10-08 17:51:10 +07:00
|
|
|
handle,
|
2020-10-16 16:29:54 +07:00
|
|
|
twitterId,
|
2020-10-07 20:15:42 +07:00
|
|
|
imageUrl,
|
2020-10-16 16:29:54 +07:00
|
|
|
image,
|
|
|
|
id,
|
2020-10-07 20:15:42 +07:00
|
|
|
];
|
|
|
|
const [result]: any = await connection.query(query, params);
|
|
|
|
connection.release();
|
|
|
|
} catch (e) {
|
2020-10-16 16:29:54 +07:00
|
|
|
logger.err('$updateDonation() error' + e);
|
2020-10-07 20:15:42 +07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-16 16:29:54 +07:00
|
|
|
private async $getTwitterUserData(handle: string): Promise<any> {
|
2020-10-07 20:15:42 +07:00
|
|
|
return new Promise((resolve, reject) => {
|
2020-10-16 16:29:54 +07:00
|
|
|
logger.debug('Fetching Twitter API data...');
|
2020-10-07 20:15:42 +07:00
|
|
|
request.get({
|
2020-10-16 16:29:54 +07:00
|
|
|
uri: `https://api.twitter.com/1.1/users/show.json?screen_name=${handle}`,
|
2020-10-07 20:15:42 +07:00
|
|
|
json: true,
|
2020-10-16 16:29:54 +07:00
|
|
|
headers: {
|
|
|
|
Authorization: 'Bearer ' + config.TWITTER_BEARER_AUTH
|
|
|
|
},
|
2020-10-07 20:15:42 +07:00
|
|
|
}, (err, res, body) => {
|
|
|
|
if (err) { return reject(err); }
|
2020-10-16 16:29:54 +07:00
|
|
|
logger.debug('Twitter user data fetched:' + JSON.stringify(body.data));
|
|
|
|
resolve(body);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private async $downloadProfileImageBlob(url: string): Promise<string> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
logger.debug('Fetching image blob...');
|
|
|
|
request.get({
|
|
|
|
uri: url,
|
|
|
|
encoding: null,
|
|
|
|
}, (err, res, body) => {
|
|
|
|
if (err) { return reject(err); }
|
|
|
|
logger.debug('Image downloaded.');
|
|
|
|
resolve(Buffer.from(body, 'utf8').toString('base64'));
|
2020-10-07 20:15:42 +07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2020-10-16 16:29:54 +07:00
|
|
|
|
2020-10-17 15:01:58 +07:00
|
|
|
private async refreshSponsors(): Promise<void> {
|
|
|
|
const oldDonations = await this.$getOldDonations();
|
|
|
|
oldDonations.forEach(async (donation: any) => {
|
2020-10-16 16:29:54 +07:00
|
|
|
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));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-10-07 20:15:42 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
export default new Donations();
|