parent
							
								
									6be353c1b3
								
							
						
					
					
						commit
						1c1cfa0cbb
					
				
							
								
								
									
										1338
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1338
									
								
								backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -25,19 +25,19 @@
 | 
			
		||||
    "test": "echo \"Error: no test specified\" && exit 1"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@types/axios": "^0.14.0",
 | 
			
		||||
    "axios": "^0.21.0",
 | 
			
		||||
    "express": "^4.17.1",
 | 
			
		||||
    "locutus": "^2.0.12",
 | 
			
		||||
    "mysql2": "^1.6.1",
 | 
			
		||||
    "node-worker-threads-pool": "^1.4.2",
 | 
			
		||||
    "request": "^2.88.2",
 | 
			
		||||
    "ws": "^7.3.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/compression": "^1.0.1",
 | 
			
		||||
    "@types/express": "^4.17.2",
 | 
			
		||||
    "@types/request": "^2.48.2",
 | 
			
		||||
    "@types/ws": "^6.0.4",
 | 
			
		||||
    "@types/locutus": "^0.0.6",
 | 
			
		||||
    "@types/ws": "^6.0.4",
 | 
			
		||||
    "tslint": "~6.1.0",
 | 
			
		||||
    "typescript": "~3.9.7"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import * as fs from 'fs';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
 | 
			
		||||
import { Common } from '../common';
 | 
			
		||||
import { Block } from '../../interfaces';
 | 
			
		||||
@ -138,18 +138,19 @@ class Bisq {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updatePrice() {
 | 
			
		||||
    request('https://bisq.markets/api/trades/?market=bsq_btc', { json: true }, (err, res, trades: BisqTrade[]) => {
 | 
			
		||||
      if (err || !Array.isArray(trades)) { return logger.err('Error updating Bisq market price: ' + err); }
 | 
			
		||||
 | 
			
		||||
      const prices: number[] = [];
 | 
			
		||||
      trades.forEach((trade) => {
 | 
			
		||||
        prices.push(parseFloat(trade.price) * 100000000);
 | 
			
		||||
      });
 | 
			
		||||
      prices.sort((a, b) => a - b);
 | 
			
		||||
      this.price = Common.median(prices);
 | 
			
		||||
      if (this.priceUpdateCallbackFunction) {
 | 
			
		||||
        this.priceUpdateCallbackFunction(this.price);
 | 
			
		||||
      }
 | 
			
		||||
    axios.get<BisqTrade[]>('https://bisq.markets/api/trades/?market=bsq_btc')
 | 
			
		||||
      .then((response) => {
 | 
			
		||||
        const prices: number[] = [];
 | 
			
		||||
        response.data.forEach((trade) => {
 | 
			
		||||
          prices.push(parseFloat(trade.price) * 100000000);
 | 
			
		||||
        });
 | 
			
		||||
        prices.sort((a, b) => a - b);
 | 
			
		||||
        this.price = Common.median(prices);
 | 
			
		||||
        if (this.priceUpdateCallbackFunction) {
 | 
			
		||||
          this.priceUpdateCallbackFunction(this.price);
 | 
			
		||||
        }
 | 
			
		||||
    }).catch((err) => {
 | 
			
		||||
      logger.err('Error updating Bisq market price: ' + err);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { Transaction, Block, MempoolInfo } from '../../interfaces';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
class ElectrsApi {
 | 
			
		||||
 | 
			
		||||
@ -8,138 +8,48 @@ class ElectrsApi {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMempoolInfo(): Promise<MempoolInfo> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS.REST_API_URL + '/mempool', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject('getMempoolInfo error: ' + err.message || err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          if (typeof response.count !== 'number') {
 | 
			
		||||
            reject('Empty data');
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          resolve({
 | 
			
		||||
            size: response.count,
 | 
			
		||||
            bytes: response.vsize,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
    return axios.get<any>(config.ELECTRS.REST_API_URL + '/mempool', { timeout: 10000 })
 | 
			
		||||
      .then((response) => {
 | 
			
		||||
        return {
 | 
			
		||||
          size: response.data.count,
 | 
			
		||||
          bytes: response.data.vsize,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawMempool(): Promise<Transaction['txid'][]> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS.REST_API_URL + '/mempool/txids', { json: true, timeout: 10000, forever: true }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject('getRawMempool error: ' + err.message || err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          if (response.constructor === Array) {
 | 
			
		||||
            resolve(response);
 | 
			
		||||
          } else {
 | 
			
		||||
            reject('returned invalid data');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return axios.get<Transaction['txid'][]>(config.ELECTRS.REST_API_URL + '/mempool/txids')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawTransaction(txId: string): Promise<Transaction> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS.REST_API_URL + '/tx/' + txId, { json: true, timeout: 10000, forever: true }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject('getRawTransaction error: ' + err.message || err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          if (response.constructor === Object) {
 | 
			
		||||
            resolve(response);
 | 
			
		||||
          } else {
 | 
			
		||||
            reject('returned invalid data');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return axios.get<Transaction>(config.ELECTRS.REST_API_URL + '/tx/' + txId)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS.REST_API_URL + '/blocks/tip/height', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject('getBlockHeightTip error: ' + err.message || err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return axios.get<number>(config.ELECTRS.REST_API_URL + '/blocks/tip/height')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS.REST_API_URL + '/block/' + hash + '/txids', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject('getTxIdsForBlock error: ' + err.message || err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          if (response.constructor === Array) {
 | 
			
		||||
            resolve(response);
 | 
			
		||||
          } else {
 | 
			
		||||
            reject('returned invalid data');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return axios.get<string[]>(config.ELECTRS.REST_API_URL + '/block/' + hash + '/txids')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS.REST_API_URL + '/block-height/' + height, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject('getBlockHash error: ' + err.message || err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else  {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return axios.get<string>(config.ELECTRS.REST_API_URL + '/block-height/' + height)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlocksFromHeight(height: number): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS.REST_API_URL + '/blocks/' + height, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject('getBlocksFromHeight error: ' + err.message || err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else  {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return axios.get<string>(config.ELECTRS.REST_API_URL + '/blocks/' + height)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlock(hash: string): Promise<Block> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS.REST_API_URL + '/block/' + hash, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject('getBlock error: ' + err.message || err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          if (response.constructor === Object) {
 | 
			
		||||
            resolve(response);
 | 
			
		||||
          } else {
 | 
			
		||||
            reject('getBlock returned invalid data');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    return axios.get<Block>(config.ELECTRS.REST_API_URL + '/block/' + hash)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,12 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
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,
 | 
			
		||||
    baseURL: config.SPONSORS.BTCPAY_URL,
 | 
			
		||||
    headers: {
 | 
			
		||||
      'Content-Type': 'application/json',
 | 
			
		||||
      'Authorization': config.SPONSORS.BTCPAY_AUTH,
 | 
			
		||||
@ -34,7 +34,7 @@ class Donations {
 | 
			
		||||
    this.notifyDonationStatusCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $createRequest(amount: number, orderId: string): Promise<any> {
 | 
			
		||||
  async $createRequest(amount: number, orderId: string): Promise<any> {
 | 
			
		||||
    logger.notice('New invoice request. Handle: ' + orderId + ' Amount: ' + amount + ' BTC');
 | 
			
		||||
 | 
			
		||||
    const postData = {
 | 
			
		||||
@ -43,30 +43,21 @@ class Donations {
 | 
			
		||||
      'currency': 'BTC',
 | 
			
		||||
      'itemDesc': 'Sponsor mempool.space',
 | 
			
		||||
      'notificationUrl': config.SPONSORS.BTCPAY_WEBHOOK_URL,
 | 
			
		||||
      'redirectURL': 'https://mempool.space/about'
 | 
			
		||||
      '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,
 | 
			
		||||
    };
 | 
			
		||||
    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),
 | 
			
		||||
          addresses: body.data.addresses,
 | 
			
		||||
        };
 | 
			
		||||
        resolve(formattedBody);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $handleWebhookRequest(data: any): Promise<void> {
 | 
			
		||||
    if (!data || !data.id) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const response = await this.getStatus(data.id);
 | 
			
		||||
    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;
 | 
			
		||||
@ -128,19 +119,11 @@ class Donations {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getStatus(id: string): Promise<any> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      logger.debug('Fetching status for invoice: ' + id);
 | 
			
		||||
      request.get({
 | 
			
		||||
        uri: '/invoices/' + id,
 | 
			
		||||
        json: true,
 | 
			
		||||
        ...this.options,
 | 
			
		||||
      }, (err, res, body) => {
 | 
			
		||||
        if (err) { return reject(err); }
 | 
			
		||||
        logger.debug('Invoice status received: ' + JSON.stringify(body.data));
 | 
			
		||||
        resolve(body.data);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  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,
 | 
			
		||||
@ -182,34 +165,21 @@ class Donations {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getTwitterUserData(handle: string): Promise<any> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      logger.debug('Fetching Twitter API data...');
 | 
			
		||||
      request.get({
 | 
			
		||||
        uri: `https://api.twitter.com/1.1/users/show.json?screen_name=${handle}`,
 | 
			
		||||
        json: true,
 | 
			
		||||
        headers: {
 | 
			
		||||
          Authorization: 'Bearer ' + config.SPONSORS.TWITTER_BEARER_AUTH
 | 
			
		||||
        },
 | 
			
		||||
      }, (err, res, body) => {
 | 
			
		||||
        if (err) { return reject(err); }
 | 
			
		||||
        logger.debug('Twitter user data fetched:' + JSON.stringify(body.data));
 | 
			
		||||
        resolve(body);
 | 
			
		||||
      });
 | 
			
		||||
    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
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    logger.debug('Twitter user data fetched:' + JSON.stringify(res.data));
 | 
			
		||||
    return res.data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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'));
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    logger.debug('Fetching image blob...');
 | 
			
		||||
    const res = await axios.get(url, { responseType: 'arraybuffer' });
 | 
			
		||||
    logger.debug('Image downloaded.');
 | 
			
		||||
    return Buffer.from(res.data, 'utf8').toString('base64');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async refreshSponsors(): Promise<void> {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
class FiatConversion {
 | 
			
		||||
  private tickers = {
 | 
			
		||||
@ -20,13 +20,13 @@ class FiatConversion {
 | 
			
		||||
    return this.tickers;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private updateCurrency() {
 | 
			
		||||
    request('https://api.opennode.co/v1/rates', { json: true }, (err, res, body) => {
 | 
			
		||||
      if (err) { return logger.err('Error updating currency from OpenNode: ' + err); }
 | 
			
		||||
      if (body && body.data) {
 | 
			
		||||
        this.tickers = body.data;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  private async updateCurrency(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await axios.get('https://api.opennode.co/v1/rates');
 | 
			
		||||
      this.tickers = response.data.data;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Error updating currency from OpenNode: ' + e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import * as http from 'http';
 | 
			
		||||
import * as https from 'https';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
import * as cluster from 'cluster';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
import { checkDbConnection } from './database';
 | 
			
		||||
import config from './config';
 | 
			
		||||
@ -190,11 +190,13 @@ class Server {
 | 
			
		||||
      ;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.app
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'donations', (req, res) => {
 | 
			
		||||
          req.pipe(request('https://mempool.space/api/v1/donations')).pipe(res);
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
 | 
			
		||||
          const response = await axios.get('https://mempool.space/api/v1/donations', { responseType: 'stream' });
 | 
			
		||||
          response.data.pipe(res);
 | 
			
		||||
        })
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', (req, res) => {
 | 
			
		||||
          req.pipe(request('https://mempool.space/api/v1/donations/images/' + req.params.id)).pipe(res);
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
 | 
			
		||||
          const response = await axios.get('https://mempool.space/api/v1/donations/images/' + req.params.id, { responseType: 'stream' });
 | 
			
		||||
          response.data.pipe(res);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user