Removing sponsors code.
Support new sponsor confirmation polling. fixes #319
This commit is contained in:
		
							parent
							
								
									d0edb3ff92
								
							
						
					
					
						commit
						9651fa7859
					
				@ -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": ""
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "/api/v1/donations": {
 | 
			
		||||
    "target": "http://localhost:9000/",
 | 
			
		||||
    "secure": false
 | 
			
		||||
  },
 | 
			
		||||
  "/api/v1": {
 | 
			
		||||
    "target": "http://localhost:8999/",
 | 
			
		||||
    "secure": false
 | 
			
		||||
 | 
			
		||||
@ -173,10 +173,9 @@
 | 
			
		||||
 | 
			
		||||
    <div *ngIf="donationStatus === 4" class="text-center">
 | 
			
		||||
      <h2><span i18n="about.sponsor.donation-confirmed">Donation confirmed!</span><br><span i18n="about.sponsor.thank-you">Thank you!</span></h2>
 | 
			
		||||
      <p i18n="about.sponsor.sponsor-completed">If you specified a Twitter handle, the profile photo should now be visible on this page when you reload.</p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <br><br><br><br>
 | 
			
		||||
    <br><br><br>
 | 
			
		||||
 | 
			
		||||
    <a target="_blank" class="m-2 fw6 mb3 mt2 truncate black-80 f4 link" href="https://github.com/mempool/mempool">
 | 
			
		||||
      <span class="dib v-mid">
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,19 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { Component, OnDestroy, OnInit } from '@angular/core';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { Observable, Subscription } from 'rxjs';
 | 
			
		||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
 | 
			
		||||
import { map } from 'rxjs/operators';
 | 
			
		||||
import { delay, map, retryWhen, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-about',
 | 
			
		||||
  templateUrl: './about.component.html',
 | 
			
		||||
  styleUrls: ['./about.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class AboutComponent implements OnInit {
 | 
			
		||||
export class AboutComponent implements OnInit, OnDestroy {
 | 
			
		||||
  gitCommit$: Observable<string>;
 | 
			
		||||
  donationForm: FormGroup;
 | 
			
		||||
  paymentForm: FormGroup;
 | 
			
		||||
@ -22,6 +22,7 @@ export class AboutComponent implements OnInit {
 | 
			
		||||
  donationObj: any;
 | 
			
		||||
  sponsorsEnabled = this.stateService.env.SPONSORS_ENABLED;
 | 
			
		||||
  sponsors = null;
 | 
			
		||||
  requestSubscription: Subscription | undefined;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
@ -50,23 +51,37 @@ export class AboutComponent implements OnInit {
 | 
			
		||||
      .subscribe((sponsors) => {
 | 
			
		||||
        this.sponsors = sponsors;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    this.apiService.getDonation$()
 | 
			
		||||
    this.stateService.donationConfirmed$.subscribe(() => this.donationStatus = 4);
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    if (this.requestSubscription) {
 | 
			
		||||
      this.requestSubscription.unsubscribe();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  submitDonation() {
 | 
			
		||||
    if (this.donationForm.invalid) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this.apiService.requestDonation$(
 | 
			
		||||
    this.requestSubscription = this.apiService.requestDonation$(
 | 
			
		||||
      this.donationForm.get('amount').value,
 | 
			
		||||
      this.donationForm.get('handle').value
 | 
			
		||||
    )
 | 
			
		||||
    .subscribe((response) => {
 | 
			
		||||
      this.websocketService.trackDonation(response.id);
 | 
			
		||||
      this.donationObj = response;
 | 
			
		||||
      this.donationStatus = 3;
 | 
			
		||||
    .pipe(
 | 
			
		||||
      tap((response) => {
 | 
			
		||||
        this.donationObj = response;
 | 
			
		||||
        this.donationStatus = 3;
 | 
			
		||||
      }),
 | 
			
		||||
      switchMap(() => this.apiService.checkDonation$(this.donationObj.id)
 | 
			
		||||
        .pipe(
 | 
			
		||||
          retryWhen((errors) => errors.pipe(delay(2000)))
 | 
			
		||||
        )
 | 
			
		||||
      )
 | 
			
		||||
    ).subscribe(() => {
 | 
			
		||||
      this.donationStatus = 4;
 | 
			
		||||
      if (this.donationForm.get('handle').value) {
 | 
			
		||||
        this.sponsors.unshift({ handle: this.donationForm.get('handle').value });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -15,13 +15,11 @@ export interface WebsocketResponse {
 | 
			
		||||
  tx?: Transaction;
 | 
			
		||||
  rbfTransaction?: Transaction;
 | 
			
		||||
  transactions?: TransactionStripped[];
 | 
			
		||||
  donationConfirmed?: boolean;
 | 
			
		||||
  loadingIndicators?: ILoadingIndicators;
 | 
			
		||||
  'track-tx'?: string;
 | 
			
		||||
  'track-address'?: string;
 | 
			
		||||
  'track-asset'?: string;
 | 
			
		||||
  'watch-mempool'?: boolean;
 | 
			
		||||
  'track-donation'?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
 | 
			
		||||
@ -77,6 +77,10 @@ export class ApiService {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  checkDonation$(orderId: string): Observable<any[]> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/donations/check?order_id=' + orderId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getInitData$(): Observable<WebsocketResponse> {
 | 
			
		||||
    return this.httpClient.get<WebsocketResponse>(this.apiBaseUrl + this.apiBasePath + '/api/v1/init-data');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -64,7 +64,6 @@ export class StateService {
 | 
			
		||||
  vbytesPerSecond$ = new ReplaySubject<number>(1);
 | 
			
		||||
  lastDifficultyAdjustment$ = new ReplaySubject<number>(1);
 | 
			
		||||
  gitCommit$ = new ReplaySubject<string>(1);
 | 
			
		||||
  donationConfirmed$ = new Subject();
 | 
			
		||||
  loadingIndicators$ = new ReplaySubject<ILoadingIndicators>(1);
 | 
			
		||||
 | 
			
		||||
  live2Chart$ = new Subject<OptimizedMempoolStats>();
 | 
			
		||||
 | 
			
		||||
@ -126,10 +126,6 @@ export class WebsocketService {
 | 
			
		||||
    this.isTrackingTx = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackDonation(id: string) {
 | 
			
		||||
    this.websocketSubject.next({ 'track-donation': id });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stopTrackingTransaction() {
 | 
			
		||||
    if (!this.isTrackingTx) {
 | 
			
		||||
      return;
 | 
			
		||||
@ -289,9 +285,5 @@ export class WebsocketService {
 | 
			
		||||
    if (response['git-commit']) {
 | 
			
		||||
      this.stateService.gitCommit$.next(response['git-commit']);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.donationConfirmed) {
 | 
			
		||||
      this.stateService.donationConfirmed$.next(true);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -46,7 +46,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">205</context>
 | 
			
		||||
          <context context-type="linenumber">208</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/bisq/bisq-transfers/bisq-transfers.component.html</context>
 | 
			
		||||
@ -67,7 +67,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">206</context>
 | 
			
		||||
          <context context-type="linenumber">209</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/bisq/bisq-transfers/bisq-transfers.component.html</context>
 | 
			
		||||
@ -88,7 +88,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">209</context>
 | 
			
		||||
          <context context-type="linenumber">212</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">Transaction unconfirmed state</note>
 | 
			
		||||
        <note priority="1" from="meaning">transaction.unconfirmed</note>
 | 
			
		||||
@ -242,7 +242,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">198</context>
 | 
			
		||||
          <context context-type="linenumber">201</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/block/block.component.html</context>
 | 
			
		||||
@ -468,7 +468,7 @@
 | 
			
		||||
        <source>nSequence</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">92</context>
 | 
			
		||||
          <context context-type="linenumber">95</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">transactions-list.nsequence</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -476,7 +476,7 @@
 | 
			
		||||
        <source>ScriptSig (ASM)</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">71</context>
 | 
			
		||||
          <context context-type="linenumber">74</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">ScriptSig (ASM)</note>
 | 
			
		||||
        <note priority="1" from="meaning">transactions-list.scriptsig.asm</note>
 | 
			
		||||
@ -485,7 +485,7 @@
 | 
			
		||||
        <source>ScriptSig (HEX)</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">75</context>
 | 
			
		||||
          <context context-type="linenumber">78</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">ScriptSig (HEX)</note>
 | 
			
		||||
        <note priority="1" from="meaning">transactions-list.scriptsig.hex</note>
 | 
			
		||||
@ -494,7 +494,7 @@
 | 
			
		||||
        <source>Witness</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">80</context>
 | 
			
		||||
          <context context-type="linenumber">83</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">transactions-list.witness</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -502,7 +502,7 @@
 | 
			
		||||
        <source>P2SH redeem script</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">84</context>
 | 
			
		||||
          <context context-type="linenumber">87</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">transactions-list.p2sh-redeem-script</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -510,7 +510,7 @@
 | 
			
		||||
        <source>P2WSH witness script</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">88</context>
 | 
			
		||||
          <context context-type="linenumber">91</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">transactions-list.p2wsh-witness-script</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -518,7 +518,7 @@
 | 
			
		||||
        <source>Previous output script</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">96</context>
 | 
			
		||||
          <context context-type="linenumber">99</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">transactions-list.previous-output-script</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -526,11 +526,11 @@
 | 
			
		||||
        <source>Load all</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">106</context>
 | 
			
		||||
          <context context-type="linenumber">109</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">188</context>
 | 
			
		||||
          <context context-type="linenumber">191</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">transactions-list.load-all</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -538,7 +538,7 @@
 | 
			
		||||
        <source>Peg-out to <x id="START_TAG_NG_CONTAINER" ctype="x-ng_container" equiv-text="<ng-container *ngTemplateOutlet="pegOutLink">"/><x id="CLOSE_TAG_NG_CONTAINER" ctype="x-ng_container" equiv-text="</ng-container>"/></source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">125</context>
 | 
			
		||||
          <context context-type="linenumber">128</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">transactions-list.peg-out-to</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -546,7 +546,7 @@
 | 
			
		||||
        <source>ScriptPubKey (ASM)</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">170</context>
 | 
			
		||||
          <context context-type="linenumber">173</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">ScriptPubKey (ASM)</note>
 | 
			
		||||
        <note priority="1" from="meaning">transactions-list.scriptpubkey.asm</note>
 | 
			
		||||
@ -555,7 +555,7 @@
 | 
			
		||||
        <source>ScriptPubKey (HEX)</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">174</context>
 | 
			
		||||
          <context context-type="linenumber">177</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">ScriptPubKey (HEX)</note>
 | 
			
		||||
        <note priority="1" from="meaning">transactions-list.scriptpubkey.hex</note>
 | 
			
		||||
@ -564,7 +564,7 @@
 | 
			
		||||
        <source>Type</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">166</context>
 | 
			
		||||
          <context context-type="linenumber">169</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/bisq/bisq-transaction-details/bisq-transaction-details.component.html</context>
 | 
			
		||||
@ -580,7 +580,7 @@
 | 
			
		||||
        <source>data</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">178</context>
 | 
			
		||||
          <context context-type="linenumber">181</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">transactions-list.vout.scriptpubkey-type.data</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -588,7 +588,7 @@
 | 
			
		||||
        <source>sat</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">198</context>
 | 
			
		||||
          <context context-type="linenumber">201</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">sat</note>
 | 
			
		||||
        <note priority="1" from="meaning">shared.sat</note>
 | 
			
		||||
@ -597,7 +597,7 @@
 | 
			
		||||
        <source>Confidential</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/transactions-list/transactions-list.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">214</context>
 | 
			
		||||
          <context context-type="linenumber">217</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/amount/amount.component.html</context>
 | 
			
		||||
@ -605,7 +605,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address/address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">132</context>
 | 
			
		||||
          <context context-type="linenumber">134</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/asset/asset.component.html</context>
 | 
			
		||||
@ -841,11 +841,23 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">shared.address</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7e69426bd97a606d8ae6026762858e6e7c86a1fd" datatype="html">
 | 
			
		||||
        <source>Balance</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address/address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">30</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/bisq/bisq-address/bisq-address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">28</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">address.balance</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="a9b87c3aa4731edee661c8287ef3aab71799c0b8" datatype="html">
 | 
			
		||||
        <source>Total received</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address/address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">20</context>
 | 
			
		||||
          <context context-type="linenumber">21</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/bisq/bisq-address/bisq-address.component.html</context>
 | 
			
		||||
@ -857,7 +869,7 @@
 | 
			
		||||
        <source>Total sent</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address/address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">24</context>
 | 
			
		||||
          <context context-type="linenumber">25</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/bisq/bisq-address/bisq-address.component.html</context>
 | 
			
		||||
@ -869,23 +881,11 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">address.total-sent</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="7e69426bd97a606d8ae6026762858e6e7c86a1fd" datatype="html">
 | 
			
		||||
        <source>Balance</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address/address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">28</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/bisq/bisq-address/bisq-address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">28</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">address.balance</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="714e34125b3343df73f19ec800b43be95217d5d4" datatype="html">
 | 
			
		||||
        <source><x id="INTERPOLATION" equiv-text="{{ (transactions?.length | number) || '?' }}"/> of <x id="INTERPOLATION_1" equiv-text="{{ txCount | number }}"/> transaction</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address/address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">48</context>
 | 
			
		||||
          <context context-type="linenumber">50</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">X of X Address Transaction</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -893,7 +893,7 @@
 | 
			
		||||
        <source><x id="INTERPOLATION" equiv-text="{{ (transactions?.length | number) || '?' }}"/> of <x id="INTERPOLATION_1" equiv-text="{{ txCount | number }}"/> transactions</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address/address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">49</context>
 | 
			
		||||
          <context context-type="linenumber">51</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">X of X Address Transactions (Plural)</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -901,7 +901,7 @@
 | 
			
		||||
        <source>Error loading address data.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address/address.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">113</context>
 | 
			
		||||
          <context context-type="linenumber">115</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">address.error.loading-address-data</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -1020,15 +1020,23 @@
 | 
			
		||||
        <source>multisig <x id="INTERPOLATION" equiv-text="{{ multisigM }}"/> of <x id="INTERPOLATION_1" equiv-text="{{ multisigN }}"/></source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address-labels/address-labels.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">1</context>
 | 
			
		||||
          <context context-type="linenumber">5</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">address-labels.multisig</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="aae004b987aaf258dea1829618651427b68283db" datatype="html">
 | 
			
		||||
        <source>Layer<x id="INTERPOLATION" equiv-text="{{ network === 'liquid' ? '3' : '2' }}"/> Peg-out</source>
 | 
			
		||||
      <trans-unit id="31c09dcc0ab351767631539b208d5f7de4005473" datatype="html">
 | 
			
		||||
        <source>Lightning <x id="INTERPOLATION" equiv-text="{{ lightning }}"/></source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address-labels/address-labels.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">2</context>
 | 
			
		||||
          <context context-type="linenumber">11</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">address-labels.upper-layer-peg-out</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="696ade981a05c12e10df38ba6218c76e318813b3" datatype="html">
 | 
			
		||||
        <source>Liquid <x id="INTERPOLATION" equiv-text="{{ liquid }}"/></source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/address-labels/address-labels.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">17</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">address-labels.upper-layer-peg-out</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -1052,7 +1060,7 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/about/about.component.ts</context>
 | 
			
		||||
          <context context-type="linenumber">37</context>
 | 
			
		||||
          <context context-type="linenumber">38</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">master-page.about</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
@ -1199,7 +1207,7 @@
 | 
			
		||||
        <source>Terms of Service</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/about/about.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">206</context>
 | 
			
		||||
          <context context-type="linenumber">205</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/dashboard/dashboard.component.html</context>
 | 
			
		||||
@ -1268,14 +1276,6 @@
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">about.sponsor.thank-you</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="981a90db6601297002689e5fae09d38b9e374b05" datatype="html">
 | 
			
		||||
        <source>If you specified a Twitter handle, the profile photo should now be visible on this page when you reload.</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
          <context context-type="sourcefile">src/app/components/about/about.component.html</context>
 | 
			
		||||
          <context context-type="linenumber">176</context>
 | 
			
		||||
        </context-group>
 | 
			
		||||
        <note priority="1" from="description">about.sponsor.sponsor-completed</note>
 | 
			
		||||
      </trans-unit>
 | 
			
		||||
      <trans-unit id="ff4b7f4070be9e876c7610d99b9dbd53ff19dceb" datatype="html">
 | 
			
		||||
        <source>Loading graphs...</source>
 | 
			
		||||
        <context-group purpose="location">
 | 
			
		||||
 | 
			
		||||
@ -84,24 +84,3 @@ ALTER TABLE `transactions`
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `statistics`
 | 
			
		||||
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CREATE TABLE `donations` (
 | 
			
		||||
  `id` int(11) NOT NULL,
 | 
			
		||||
  `added` datetime NOT NULL,
 | 
			
		||||
  `amount` float NOT NULL,
 | 
			
		||||
  `handle` varchar(250) NOT NULL,
 | 
			
		||||
  `order_id` varchar(25) NOT NULL,
 | 
			
		||||
  `imageUrl` varchar(250) NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `donations`
 | 
			
		||||
  ADD PRIMARY KEY (`id`);
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `donations`
 | 
			
		||||
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `donations` ADD UNIQUE(`order_id`);
 | 
			
		||||
 | 
			
		||||
ALTER TABLE `donations` ADD `image` MEDIUMBLOB NULL AFTER `imageUrl`;
 | 
			
		||||
ALTER TABLE `donations` ADD `twitter_id` VARCHAR(250) NULL AFTER `handle`;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user