Merge pull request #125 from mempool/donations
Custom BTCPay donation integration
This commit is contained in:
		
						commit
						7ee04ff011
					
				@ -22,5 +22,8 @@
 | 
			
		||||
  "BISQ_MARKETS_DATA_PATH": "/bisq/seednode-data/btc_mainnet/db",
 | 
			
		||||
  "SSL": false,
 | 
			
		||||
  "SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem",
 | 
			
		||||
  "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem"
 | 
			
		||||
  "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem",
 | 
			
		||||
  "BTCPAY_URL": "",
 | 
			
		||||
  "BTCPAY_WEBHOOK_URL": "",
 | 
			
		||||
  "BTCPAY_AUTH": ""
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										132
									
								
								backend/src/api/donations.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								backend/src/api/donations.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,132 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
 | 
			
		||||
class Donations {
 | 
			
		||||
  private notifyDonationStatusCallback: ((invoiceId: string) => void) | undefined;
 | 
			
		||||
  private options = {
 | 
			
		||||
    baseUrl: config.BTCPAY_URL,
 | 
			
		||||
    headers: {
 | 
			
		||||
      'Content-Type': 'application/json',
 | 
			
		||||
      'Authorization': config.BTCPAY_AUTH,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  setNotfyDonationStatusCallback(fn: any) {
 | 
			
		||||
    this.notifyDonationStatusCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createRequest(amount: number, orderId: string): Promise<any> {
 | 
			
		||||
    const postData = {
 | 
			
		||||
      'price': amount,
 | 
			
		||||
      'orderId': orderId,
 | 
			
		||||
      'currency': 'BTC',
 | 
			
		||||
      'itemDesc': 'Sponsor mempool.space',
 | 
			
		||||
      'notificationUrl': config.BTCPAY_WEBHOOK_URL,
 | 
			
		||||
      '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);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $handleWebhookRequest(data: any) {
 | 
			
		||||
    if (!data || !data.id) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const response = await this.getStatus(data.id);
 | 
			
		||||
    if (response.status !== 'complete' && response.status !== 'confirmed') {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.notifyDonationStatusCallback) {
 | 
			
		||||
      this.notifyDonationStatusCallback(data.id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (parseFloat(response.btcPaid) < 0.001) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let imageUrl = '';
 | 
			
		||||
    if (response.orderId !== '') {
 | 
			
		||||
      try {
 | 
			
		||||
        imageUrl = await this.$getTwitterImageUrl(response.orderId);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        console.log('Error fetching twitter image', e.message);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.$addDonationToDatabase(response, imageUrl);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getStatus(id: string): Promise<any> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request.get({
 | 
			
		||||
        uri: '/invoices/' + id,
 | 
			
		||||
        json: true,
 | 
			
		||||
        ...this.options,
 | 
			
		||||
      }, (err, res, body) => {
 | 
			
		||||
        if (err) { return reject(err); }
 | 
			
		||||
        resolve(body.data);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getDonationsFromDatabase() {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = `SELECT handle, imageUrl FROM donations WHERE handle != ''`;
 | 
			
		||||
      const [rows] = await connection.query<any>(query);
 | 
			
		||||
      connection.release();
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('$getDonationsFromDatabase() error', e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $addDonationToDatabase(response: any, imageUrl: string): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = `INSERT INTO donations(added, amount, handle, order_id, imageUrl) VALUES (NOW(), ?, ?, ?, ?)`;
 | 
			
		||||
      const params: (string | number)[] = [
 | 
			
		||||
        response.btcPaid,
 | 
			
		||||
        response.orderId,
 | 
			
		||||
        response.id,
 | 
			
		||||
        imageUrl,
 | 
			
		||||
      ];
 | 
			
		||||
      const [result]: any = await connection.query(query, params);
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('$addDonationToDatabase() error', e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getTwitterImageUrl(handle: string): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request.get({
 | 
			
		||||
        uri: `https://api.hive.one/v1/influencers/screen_name/${handle}/?format=json`,
 | 
			
		||||
        json: true,
 | 
			
		||||
      }, (err, res, body) => {
 | 
			
		||||
        if (err) { return reject(err); }
 | 
			
		||||
        resolve(body.data.imageUrl);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Donations();
 | 
			
		||||
@ -99,6 +99,10 @@ class WebsocketHandler {
 | 
			
		||||
            response['pong'] = true;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage['track-donation'] && parsedMessage['track-donation'].length === 22) {
 | 
			
		||||
            client['track-donation'] = parsedMessage['track-donation'];
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (Object.keys(response).length) {
 | 
			
		||||
            client.send(JSON.stringify(response));
 | 
			
		||||
          }
 | 
			
		||||
@ -109,6 +113,21 @@ class WebsocketHandler {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNewDonation(id: string) {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.wss.clients.forEach((client: WebSocket) => {
 | 
			
		||||
      if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (client['track-donation'] === id) {
 | 
			
		||||
        client.send(JSON.stringify({ donationConfirmed: true }));
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNewStatistic(stats: OptimizedStatistic) {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
 | 
			
		||||
@ -18,6 +18,7 @@ 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';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -62,7 +63,9 @@ class Server {
 | 
			
		||||
        res.setHeader('Access-Control-Allow-Origin', '*');
 | 
			
		||||
        next();
 | 
			
		||||
      })
 | 
			
		||||
      .use(compression());
 | 
			
		||||
      .use(compression())
 | 
			
		||||
      .use(express.urlencoded({ extended: true }))
 | 
			
		||||
      .use(express.json());
 | 
			
		||||
 | 
			
		||||
    if (config.SSL === true) {
 | 
			
		||||
      const credentials = {
 | 
			
		||||
@ -122,6 +125,7 @@ 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));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setUpHttpApiRoutes() {
 | 
			
		||||
@ -163,6 +167,14 @@ class Server {
 | 
			
		||||
        .get(config.API_ENDPOINT + 'bisq/markets/volumes', routes.getBisqMarketVolumes.bind(routes))
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.BTCPAY_URL) {
 | 
			
		||||
      this.app
 | 
			
		||||
        .get(config.API_ENDPOINT + 'donations', routes.getDonations.bind(routes))
 | 
			
		||||
        .post(config.API_ENDPOINT + 'donations', routes.createDonationRequest.bind(routes))
 | 
			
		||||
        .post(config.API_ENDPOINT + 'donations-webhook', routes.donationWebhook.bind(routes))
 | 
			
		||||
      ;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@ import bisq from './api/bisq/bisq';
 | 
			
		||||
import bisqMarket from './api/bisq/markets-api';
 | 
			
		||||
import { RequiredSpec } from './interfaces';
 | 
			
		||||
import { MarketsApiError } from './api/bisq/interfaces';
 | 
			
		||||
import donations from './api/donations';
 | 
			
		||||
 | 
			
		||||
class Routes {
 | 
			
		||||
  private cache = {};
 | 
			
		||||
@ -98,6 +99,55 @@ 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.amount < 0.001) {
 | 
			
		||||
      res.status(400).send('Amount needs to be at least 0.001');
 | 
			
		||||
      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();
 | 
			
		||||
      res.json(result);
 | 
			
		||||
    } 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);
 | 
			
		||||
@ -173,7 +223,7 @@ class Routes {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const p = this.parseRequestParameters(req, constraints);
 | 
			
		||||
    const p = this.parseRequestParameters(req.query, constraints);
 | 
			
		||||
    if (p.error) {
 | 
			
		||||
      res.status(400).json(this.getBisqMarketErrorResponse(p.error));
 | 
			
		||||
      return;
 | 
			
		||||
@ -195,7 +245,7 @@ class Routes {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const p = this.parseRequestParameters(req, constraints);
 | 
			
		||||
    const p = this.parseRequestParameters(req.query, constraints);
 | 
			
		||||
    if (p.error) {
 | 
			
		||||
      res.status(400).json(this.getBisqMarketErrorResponse(p.error));
 | 
			
		||||
      return;
 | 
			
		||||
@ -254,7 +304,7 @@ class Routes {
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const p = this.parseRequestParameters(req, constraints);
 | 
			
		||||
    const p = this.parseRequestParameters(req.query, constraints);
 | 
			
		||||
    if (p.error) {
 | 
			
		||||
      res.status(400).json(this.getBisqMarketErrorResponse(p.error));
 | 
			
		||||
      return;
 | 
			
		||||
@ -281,7 +331,7 @@ class Routes {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const p = this.parseRequestParameters(req, constraints);
 | 
			
		||||
    const p = this.parseRequestParameters(req.query, constraints);
 | 
			
		||||
    if (p.error) {
 | 
			
		||||
      res.status(400).json(this.getBisqMarketErrorResponse(p.error));
 | 
			
		||||
      return;
 | 
			
		||||
@ -323,7 +373,7 @@ class Routes {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const p = this.parseRequestParameters(req, constraints);
 | 
			
		||||
    const p = this.parseRequestParameters(req.query, constraints);
 | 
			
		||||
    if (p.error) {
 | 
			
		||||
      res.status(400).json(this.getBisqMarketErrorResponse(p.error));
 | 
			
		||||
      return;
 | 
			
		||||
@ -365,7 +415,7 @@ class Routes {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const p = this.parseRequestParameters(req, constraints);
 | 
			
		||||
    const p = this.parseRequestParameters(req.query, constraints);
 | 
			
		||||
    if (p.error) {
 | 
			
		||||
      res.status(400).json(this.getBisqMarketErrorResponse(p.error));
 | 
			
		||||
      return;
 | 
			
		||||
@ -387,7 +437,7 @@ class Routes {
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const p = this.parseRequestParameters(req, constraints);
 | 
			
		||||
    const p = this.parseRequestParameters(req.query, constraints);
 | 
			
		||||
    if (p.error) {
 | 
			
		||||
      res.status(400).json(this.getBisqMarketErrorResponse(p.error));
 | 
			
		||||
      return;
 | 
			
		||||
@ -401,15 +451,15 @@ class Routes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private parseRequestParameters(req: Request, params: RequiredSpec): { [name: string]: any; } {
 | 
			
		||||
  private parseRequestParameters(requestParams: object, params: RequiredSpec): { [name: string]: any; } {
 | 
			
		||||
    const final = {};
 | 
			
		||||
    for (const i in params) {
 | 
			
		||||
      if (params.hasOwnProperty(i)) {
 | 
			
		||||
        if (params[i].required && !req.query[i]) {
 | 
			
		||||
        if (params[i].required && requestParams[i] === undefined) {
 | 
			
		||||
          return { error: i + ' parameter missing'};
 | 
			
		||||
        }
 | 
			
		||||
        if (typeof req.query[i] === 'string') {
 | 
			
		||||
          const str = (req.query[i] || '').toString().toLowerCase();
 | 
			
		||||
        if (typeof requestParams[i] === 'string') {
 | 
			
		||||
          const str = (requestParams[i] || '').toString().toLowerCase();
 | 
			
		||||
          if (params[i].types.indexOf('@number') > -1) {
 | 
			
		||||
            const number = parseInt((str).toString(), 10);
 | 
			
		||||
            final[i] = number;
 | 
			
		||||
@ -422,6 +472,8 @@ class Routes {
 | 
			
		||||
          } else {
 | 
			
		||||
            return { error: i + ' parameter invalid'};
 | 
			
		||||
          }
 | 
			
		||||
        } else if (typeof requestParams[i] === 'number') {
 | 
			
		||||
          final[i] = requestParams[i];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -4,5 +4,6 @@
 | 
			
		||||
  "BISQ_ENABLED": false,
 | 
			
		||||
  "BISQ_SEPARATE_BACKEND": false,
 | 
			
		||||
  "ELCTRS_ITEMS_PER_PAGE": 25,
 | 
			
		||||
  "KEEP_BLOCKS_AMOUNT": 8
 | 
			
		||||
  "KEEP_BLOCKS_AMOUNT": 8,
 | 
			
		||||
  "SPONSORS_ENABLED": false
 | 
			
		||||
}
 | 
			
		||||
@ -8,11 +8,11 @@
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "ws": true
 | 
			
		||||
  },
 | 
			
		||||
  "/api": {
 | 
			
		||||
  "/api/": {
 | 
			
		||||
    "target": "http://localhost:50001/",
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "pathRewrite": {
 | 
			
		||||
      "^/api": ""
 | 
			
		||||
      "^/api/": ""
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "/testnet/api/v1": {
 | 
			
		||||
@ -30,7 +30,7 @@
 | 
			
		||||
      "^/testnet/api": "/api/v1/ws"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "/testnet/api": {
 | 
			
		||||
  "/testnet/api/": {
 | 
			
		||||
    "target": "http://localhost:50001/",
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "pathRewrite": {
 | 
			
		||||
@ -45,18 +45,18 @@
 | 
			
		||||
      "^/liquid/api": "/api/v1/ws"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "/liquid/api": {
 | 
			
		||||
  "/liquid/api/": {
 | 
			
		||||
    "target": "http://localhost:50001/",
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "pathRewrite": {
 | 
			
		||||
      "^/liquid/api": ""
 | 
			
		||||
      "^/liquid/api/": ""
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "/bisq/api": {
 | 
			
		||||
  "/bisq/api/": {
 | 
			
		||||
    "target": "http://localhost:8999/",
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "pathRewrite": {
 | 
			
		||||
      "^/bisq/api": "/api/v1/bisq"
 | 
			
		||||
      "^/bisq/api/": "/api/v1/bisq"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "/bisq/api/v1/ws": {
 | 
			
		||||
@ -64,7 +64,7 @@
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "ws": true,
 | 
			
		||||
    "pathRewrite": {
 | 
			
		||||
      "^/testnet/api": "/api/v1/ws"
 | 
			
		||||
      "^/bisq/api": "/api/v1/ws"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -14,6 +14,7 @@ import { AssetsComponent } from './assets/assets.component';
 | 
			
		||||
import { StatusViewComponent } from './components/status-view/status-view.component';
 | 
			
		||||
import { DashboardComponent } from './dashboard/dashboard.component';
 | 
			
		||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
 | 
			
		||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
@ -54,6 +55,10 @@ const routes: Routes = [
 | 
			
		||||
        path: 'about',
 | 
			
		||||
        component: AboutComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'api',
 | 
			
		||||
        component: ApiDocsComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'address/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
@ -98,10 +103,6 @@ const routes: Routes = [
 | 
			
		||||
            path: 'graphs',
 | 
			
		||||
            component: StatisticsComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'about',
 | 
			
		||||
            component: AboutComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'address/:id',
 | 
			
		||||
            component: AddressComponent
 | 
			
		||||
@ -114,6 +115,10 @@ const routes: Routes = [
 | 
			
		||||
            path: 'assets',
 | 
			
		||||
            component: AssetsComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'api',
 | 
			
		||||
            component: ApiDocsComponent,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
@ -167,15 +172,15 @@ const routes: Routes = [
 | 
			
		||||
            path: 'graphs',
 | 
			
		||||
            component: StatisticsComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'about',
 | 
			
		||||
            component: AboutComponent,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'address/:id',
 | 
			
		||||
            children: [],
 | 
			
		||||
            component: AddressComponent
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            path: 'api',
 | 
			
		||||
            component: ApiDocsComponent,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
 | 
			
		||||
@ -39,6 +39,7 @@ interface Env {
 | 
			
		||||
  LIQUID_ENABLED: boolean;
 | 
			
		||||
  BISQ_ENABLED: boolean;
 | 
			
		||||
  BISQ_SEPARATE_BACKEND: boolean;
 | 
			
		||||
  SPONSORS_ENABLED: boolean;
 | 
			
		||||
  ELCTRS_ITEMS_PER_PAGE: number;
 | 
			
		||||
  KEEP_BLOCKS_AMOUNT: number;
 | 
			
		||||
}
 | 
			
		||||
@ -48,6 +49,7 @@ const defaultEnv: Env = {
 | 
			
		||||
  'LIQUID_ENABLED': false,
 | 
			
		||||
  'BISQ_ENABLED': false,
 | 
			
		||||
  'BISQ_SEPARATE_BACKEND': false,
 | 
			
		||||
  'SPONSORS_ENABLED': false,
 | 
			
		||||
  'ELCTRS_ITEMS_PER_PAGE': 25,
 | 
			
		||||
  'KEEP_BLOCKS_AMOUNT': 8
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -43,7 +43,8 @@ import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { FeesBoxComponent } from './components/fees-box/fees-box.component';
 | 
			
		||||
import { DashboardComponent } from './dashboard/dashboard.component';
 | 
			
		||||
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
 | 
			
		||||
import { faChartArea, faCube, faCubes, faDatabase, faInfo, faInfoCircle, faList, faQuestion, faQuestionCircle, faSearch, faTachometerAlt, faThList, faTv } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { faChartArea, faCogs, faCubes, faDatabase, faInfoCircle, faList, faSearch, faTachometerAlt, faThList, faTv } from '@fortawesome/free-solid-svg-icons';
 | 
			
		||||
import { ApiDocsComponent } from './components/api-docs/api-docs.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -76,6 +77,7 @@ import { faChartArea, faCube, faCubes, faDatabase, faInfo, faInfoCircle, faList,
 | 
			
		||||
    StatusViewComponent,
 | 
			
		||||
    FeesBoxComponent,
 | 
			
		||||
    DashboardComponent,
 | 
			
		||||
    ApiDocsComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    BrowserModule,
 | 
			
		||||
@ -103,6 +105,7 @@ export class AppModule {
 | 
			
		||||
    library.addIcons(faTv);
 | 
			
		||||
    library.addIcons(faTachometerAlt);
 | 
			
		||||
    library.addIcons(faCubes);
 | 
			
		||||
    library.addIcons(faCogs);
 | 
			
		||||
    library.addIcons(faThList);
 | 
			
		||||
    library.addIcons(faList);
 | 
			
		||||
    library.addIcons(faTachometerAlt);
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
 | 
			
		||||
import { BisqExplorerComponent } from './bisq-explorer/bisq-explorer.component';
 | 
			
		||||
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
 | 
			
		||||
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
 | 
			
		||||
import { ApiDocsComponent } from '../components/api-docs/api-docs.component';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
@ -43,6 +44,10 @@ const routes: Routes = [
 | 
			
		||||
        path: 'about',
 | 
			
		||||
        component: AboutComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'api',
 | 
			
		||||
        component: ApiDocsComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: '**',
 | 
			
		||||
        redirectTo: ''
 | 
			
		||||
 | 
			
		||||
@ -12,9 +12,63 @@
 | 
			
		||||
 | 
			
		||||
    <br><br>
 | 
			
		||||
 | 
			
		||||
    <iframe src='https://btcpay.wiz.biz/apps/4XrJdGCE2a8FMEkRd52YwoiwzJqP/pos' style='height: 600px; width: 400px; border: 0;'></iframe>
 | 
			
		||||
    <ng-template [ngIf]="sponsorsEnabled">
 | 
			
		||||
 | 
			
		||||
    <h2>GitHub</h2>
 | 
			
		||||
      <h2>❤️ Sponsors</h2>
 | 
			
		||||
 | 
			
		||||
      <div *ngFor="let sponsor of sponsors$ | async; let i = index" (click)="openTwitterProfile(sponsor.handle)" class="profile_photo d-inline-block" [class.ml-3]="i > 0" [ngStyle]="{'background-image': 'url(' + sponsor.imageUrl + ')'}" [title]="sponsor.handle"></div>
 | 
			
		||||
      <br><br>
 | 
			
		||||
      
 | 
			
		||||
      <button type="button" class="btn btn-primary" (click)="donationStatus = 2" [hidden]="donationStatus !== 1">Become a sponsor</button>
 | 
			
		||||
 | 
			
		||||
      <div style="max-width: 300px;" class="mx-auto" [hidden]="donationStatus !== 2">
 | 
			
		||||
        <form [formGroup]="donationForm" (submit)="submitDonation()" class="form">
 | 
			
		||||
          <div class="input-group mb-2">
 | 
			
		||||
            <div class="input-group-prepend" style="width: 42px;">
 | 
			
		||||
              <span class="input-group-text">₿</span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <input formControlName="amount" class="form-control" type="number" min="0.001" step="1E-03">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-group mb-4" *ngIf="donationForm.get('amount').value >= 0.01; else lowAmount">
 | 
			
		||||
            <div class="input-group-prepend" style="width: 42px;">
 | 
			
		||||
              <span class="input-group-text">@</span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <input formControlName="handle" class="form-control" type="text" placeholder="Twitter handle (Optional)">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-group">
 | 
			
		||||
            <button class="btn btn-primary mx-auto" type="submit">Request invoice</button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </form>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <ng-template #lowAmount>
 | 
			
		||||
        <div class="input-group mb-4 text-small">
 | 
			
		||||
          If you donate 0.01 BTC or more, your profile photo will be added to the list of sponsors above :)
 | 
			
		||||
        </div>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
 | 
			
		||||
      <div *ngIf="donationStatus === 3" class="text-center">
 | 
			
		||||
        <div class="qr-wrapper mt-2 mb-2">
 | 
			
		||||
          <app-qrcode [data]="donationObj.address + '?amount=' + donationObj.amount"></app-qrcode>
 | 
			
		||||
        </div>
 | 
			
		||||
        <br>
 | 
			
		||||
        <p style="font-size: 10px;">{{ donationObj.address }}</p>
 | 
			
		||||
        <p style="font-size: 12px;">{{ donationObj.amount }} BTC</p>
 | 
			
		||||
 | 
			
		||||
        <p>Waiting for transaction... </p>
 | 
			
		||||
        <div class="spinner-border text-light"></div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div *ngIf="donationStatus === 4" class="text-center">
 | 
			
		||||
        <h2>Donation confirmed!<br>Thank you!</h2>
 | 
			
		||||
        <p>If you specified a Twitter handle, the profile photo should now be visible on this page when you reload.</p>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <br><br>
 | 
			
		||||
 | 
			
		||||
    </ng-template>
 | 
			
		||||
 | 
			
		||||
    <h2>Open source</h2>
 | 
			
		||||
 | 
			
		||||
    <a target="_blank" class="b2812e30 f2874b88 fw6 mb3 mt2 truncate black-80 f4 link" rel="noopener noreferrer nofollow" href="https://github.com/mempool/mempool">
 | 
			
		||||
      <span class="_9e13d83d dib v-mid">
 | 
			
		||||
@ -28,98 +82,10 @@
 | 
			
		||||
      <span>github.com/mempool/mempool</span></a>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <br><br>
 | 
			
		||||
 | 
			
		||||
  <div class="text-center">
 | 
			
		||||
    <h2>API</h2>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs">
 | 
			
		||||
    <li [ngbNavItem]="1">
 | 
			
		||||
      <a ngbNavLink>Mainnet</a>
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
 | 
			
		||||
        <table class="table">
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th style="border-top: 0;">Endpoint</th>
 | 
			
		||||
            <th style="border-top: 0;">Description</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/api/v1/fees/recommended" target="_blank">GET /api/v1/fees/recommended</a></td>
 | 
			
		||||
            <td>Recommended fees</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/api/v1/fees/mempool-blocks" target="_blank">GET /api/v1/fees/mempool-blocks</a></td>
 | 
			
		||||
            <td>The current mempool blocks</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap">wss://{{ hostname }}/api/v1/ws</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <span class="text-small">
 | 
			
		||||
                Default push: <span class="code">{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</span>
 | 
			
		||||
                to express what you want pushed. Available: 'blocks', 'mempool-blocks', 'live-2h-chart' and 'stats'.
 | 
			
		||||
              </span>
 | 
			
		||||
              <br><br>
 | 
			
		||||
              <span class="text-small">
 | 
			
		||||
                Push transactions related to address: <span class="code">{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</span>
 | 
			
		||||
                to receive all new transactions containing that address as input or output. Returns an array of transactions. 'address-transactions' for new mempool transactions and 'block-transactions' for new block confirmed transactions.
 | 
			
		||||
              </span>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </table>
 | 
			
		||||
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li [ngbNavItem]="2">
 | 
			
		||||
      <a ngbNavLink>Bisq</a>
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
        
 | 
			
		||||
        <table class="table">
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th style="border-top: 0;">Endpoint</th>
 | 
			
		||||
            <th style="border-top: 0;">Description</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/stats" target="_blank">GET /bisq/api/stats</a></td>
 | 
			
		||||
            <td>Stats</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/tx/4b5417ec5ab6112bedf539c3b4f5a806ed539542d8b717e1c4470aa3180edce5" target="_blank">GET /bisq/api/tx/:txId</a></td>
 | 
			
		||||
            <td>Transaction</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/txs/0/25" target="_blank">GET /bisq/api/txs/:index/:length</a></td>
 | 
			
		||||
            <td>Transactions</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/block/000000000000000000079aa6bfa46eb8fc20474e8673d6e8a123b211236bf82d" target="_blank">GET /bisq/api/block/:hash</a></td>
 | 
			
		||||
            <td>Block</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/blocks/0/25" target="_blank">GET /bisq/api/blocks/:index/:length</a></td>
 | 
			
		||||
            <td>Blocks</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/blocks/tip/height" target="_blank">GET /bisq/api/blocks/tip/height</a></td>
 | 
			
		||||
            <td>Latest block height</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/address/B1DgwRN92rdQ9xpEVCdXRfgeqGw9X4YtrZz" target="_blank">GET /bisq/api/address/:address</a></td>
 | 
			
		||||
            <td>Address</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </table>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </li>
 | 
			
		||||
  </ul>
 | 
			
		||||
 | 
			
		||||
  <div [ngbNavOutlet]="nav" class="mt-2"></div>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <div class="text-small text-center">
 | 
			
		||||
    Git commit: {{ gitCommit$ | async }}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,17 @@
 | 
			
		||||
.qr-wrapper {
 | 
			
		||||
  background-color: #FFF;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.profile_photo {
 | 
			
		||||
  width: 80px;
 | 
			
		||||
  height: 80px;
 | 
			
		||||
  background-size: 100%, 100%;
 | 
			
		||||
  border-radius: 50%;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text-small {
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.code {
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
  font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr {
 | 
			
		||||
  white-space: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nowrap {
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -3,33 +3,61 @@ 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 { FormBuilder, FormGroup, Validators } from '@angular/forms';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { env } from '../../app.constants';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-about',
 | 
			
		||||
  templateUrl: './about.component.html',
 | 
			
		||||
  styleUrls: ['./about.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class AboutComponent implements OnInit {
 | 
			
		||||
  active = 1;
 | 
			
		||||
  hostname = document.location.hostname;
 | 
			
		||||
  gitCommit$: Observable<string>;
 | 
			
		||||
  donationForm: FormGroup;
 | 
			
		||||
  donationStatus = 1;
 | 
			
		||||
  sponsors$: Observable<any>;
 | 
			
		||||
  donationObj: any;
 | 
			
		||||
  sponsorsEnabled = env.SPONSORS_ENABLED;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private seoService: SeoService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
    private formBuilder: FormBuilder,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.gitCommit$ = this.stateService.gitCommit$;
 | 
			
		||||
    this.seoService.setTitle('About');
 | 
			
		||||
    this.websocketService.want(['blocks']);
 | 
			
		||||
    if (this.stateService.network === 'bisq') {
 | 
			
		||||
      this.active = 2;
 | 
			
		||||
    }
 | 
			
		||||
    if (document.location.port !== '') {
 | 
			
		||||
      this.hostname = this.hostname + ':' + document.location.port;
 | 
			
		||||
 | 
			
		||||
    this.donationForm = this.formBuilder.group({
 | 
			
		||||
      amount: [0.01],
 | 
			
		||||
      handle: [''],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.sponsors$ = this.apiService.getDonation$();
 | 
			
		||||
    this.stateService.donationConfirmed$.subscribe(() => this.donationStatus = 4);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  submitDonation() {
 | 
			
		||||
    if (this.donationForm.invalid) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    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;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openTwitterProfile(handle: string) {
 | 
			
		||||
    window.open('https://twitter.com/' + handle, '_blank');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										88
									
								
								frontend/src/app/components/api-docs/api-docs.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								frontend/src/app/components/api-docs/api-docs.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
			
		||||
<div class="container-xl">
 | 
			
		||||
  <div class="text-center">
 | 
			
		||||
    <h2>API documentation</h2>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <ul ngbNav #nav="ngbNav" [(activeId)]="active" class="nav-tabs">
 | 
			
		||||
    <li [ngbNavItem]="1">
 | 
			
		||||
      <a ngbNavLink>Mainnet</a>
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
 | 
			
		||||
        <table class="table">
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th style="border-top: 0;">Endpoint</th>
 | 
			
		||||
            <th style="border-top: 0;">Description</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/api/v1/fees/recommended" target="_blank">GET /api/v1/fees/recommended</a></td>
 | 
			
		||||
            <td>Recommended fees</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/api/v1/fees/mempool-blocks" target="_blank">GET /api/v1/fees/mempool-blocks</a></td>
 | 
			
		||||
            <td>The current mempool blocks</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap">wss://{{ hostname }}/api/v1/ws</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <span class="text-small">
 | 
			
		||||
                Default push: <span class="code">{{ '{' }} action: 'want', data: ['blocks', ...] {{ '}' }}</span>
 | 
			
		||||
                to express what you want pushed. Available: 'blocks', 'mempool-blocks', 'live-2h-chart' and 'stats'.
 | 
			
		||||
              </span>
 | 
			
		||||
              <br><br>
 | 
			
		||||
              <span class="text-small">
 | 
			
		||||
                Push transactions related to address: <span class="code">{{ '{' }} 'track-address': '3PbJ...bF9B' {{ '}' }}</span>
 | 
			
		||||
                to receive all new transactions containing that address as input or output. Returns an array of transactions. 'address-transactions' for new mempool transactions and 'block-transactions' for new block confirmed transactions.
 | 
			
		||||
              </span>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </table>
 | 
			
		||||
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li [ngbNavItem]="2">
 | 
			
		||||
      <a ngbNavLink>Bisq</a>
 | 
			
		||||
      <ng-template ngbNavContent>
 | 
			
		||||
        
 | 
			
		||||
        <table class="table">
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th style="border-top: 0;">Endpoint</th>
 | 
			
		||||
            <th style="border-top: 0;">Description</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/stats" target="_blank">GET /bisq/api/stats</a></td>
 | 
			
		||||
            <td>Stats</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/tx/4b5417ec5ab6112bedf539c3b4f5a806ed539542d8b717e1c4470aa3180edce5" target="_blank">GET /bisq/api/tx/:txId</a></td>
 | 
			
		||||
            <td>Transaction</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/txs/0/25" target="_blank">GET /bisq/api/txs/:index/:length</a></td>
 | 
			
		||||
            <td>Transactions</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/block/000000000000000000079aa6bfa46eb8fc20474e8673d6e8a123b211236bf82d" target="_blank">GET /bisq/api/block/:hash</a></td>
 | 
			
		||||
            <td>Block</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/blocks/0/25" target="_blank">GET /bisq/api/blocks/:index/:length</a></td>
 | 
			
		||||
            <td>Blocks</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/blocks/tip/height" target="_blank">GET /bisq/api/blocks/tip/height</a></td>
 | 
			
		||||
            <td>Latest block height</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <td class="nowrap"><a href="/bisq/api/address/B1DgwRN92rdQ9xpEVCdXRfgeqGw9X4YtrZz" target="_blank">GET /bisq/api/address/:address</a></td>
 | 
			
		||||
            <td>Address</td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </table>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </li>
 | 
			
		||||
  </ul>
 | 
			
		||||
 | 
			
		||||
  <div [ngbNavOutlet]="nav" class="mt-2"></div>
 | 
			
		||||
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
							
								
								
									
										16
									
								
								frontend/src/app/components/api-docs/api-docs.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/src/app/components/api-docs/api-docs.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
.text-small {
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.code {
 | 
			
		||||
  background-color: #1d1f31;
 | 
			
		||||
  font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr {
 | 
			
		||||
  white-space: inherit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nowrap {
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								frontend/src/app/components/api-docs/api-docs.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/app/components/api-docs/api-docs.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-api-docs',
 | 
			
		||||
  templateUrl: './api-docs.component.html',
 | 
			
		||||
  styleUrls: ['./api-docs.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class ApiDocsComponent implements OnInit {
 | 
			
		||||
  hostname = document.location.hostname;
 | 
			
		||||
  active = 1;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    if (this.stateService.network === 'bisq') {
 | 
			
		||||
      this.active = 2;
 | 
			
		||||
    }
 | 
			
		||||
    if (document.location.port !== '') {
 | 
			
		||||
      this.hostname = this.hostname + ':' + document.location.port;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -52,9 +52,12 @@
 | 
			
		||||
      <li *ngIf="network.val === 'liquid'" class="nav-item" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/liquid/assets']" (click)="collapse()"><fa-icon [icon]="['fas', 'database']" [fixedWidth]="true" title="Assets"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active">
 | 
			
		||||
      <li *ngIf="network.val === ''" class="nav-item" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/about' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" title="About"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item mr-2" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" [routerLink]="['/api' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'cogs']" [fixedWidth]="true" title="API"></fa-icon></a>
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <app-search-form location="top" (searchTriggered)="collapse()"></app-search-form>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -14,10 +14,12 @@ export interface WebsocketResponse {
 | 
			
		||||
  tx?: Transaction;
 | 
			
		||||
  rbfTransaction?: Transaction;
 | 
			
		||||
  transactions?: TransactionStripped[];
 | 
			
		||||
  donationConfirmed?: boolean;
 | 
			
		||||
  'track-tx'?: string;
 | 
			
		||||
  'track-address'?: string;
 | 
			
		||||
  'track-asset'?: string;
 | 
			
		||||
  'watch-mempool'?: boolean;
 | 
			
		||||
  'track-donation'?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
 | 
			
		||||
@ -61,4 +61,16 @@ export class ApiService {
 | 
			
		||||
    });
 | 
			
		||||
    return this.httpClient.get<number[]>(this.apiBaseUrl + '/transaction-times', { params });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  requestDonation$(amount: number, orderId: string): Observable<any> {
 | 
			
		||||
    const params = {
 | 
			
		||||
      amount: amount,
 | 
			
		||||
      orderId: orderId,
 | 
			
		||||
    };
 | 
			
		||||
    return this.httpClient.post<any>(this.apiBaseUrl + '/donations', params);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getDonation$(): Observable<any[]> {
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + '/donations');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -34,6 +34,7 @@ export class StateService {
 | 
			
		||||
  vbytesPerSecond$ = new ReplaySubject<number>(1);
 | 
			
		||||
  lastDifficultyAdjustment$ = new ReplaySubject<number>(1);
 | 
			
		||||
  gitCommit$ = new ReplaySubject<string>(1);
 | 
			
		||||
  donationConfirmed$ = new Subject();
 | 
			
		||||
 | 
			
		||||
  live2Chart$ = new Subject<OptimizedMempoolStats>();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -155,6 +155,10 @@ export class WebsocketService {
 | 
			
		||||
          this.stateService.gitCommit$.next(response['git-commit']);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (response.donationConfirmed) {
 | 
			
		||||
          this.stateService.donationConfirmed$.next(true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.goneOffline === true) {
 | 
			
		||||
          this.goneOffline = false;
 | 
			
		||||
          if (this.lastWant) {
 | 
			
		||||
@ -189,6 +193,10 @@ export class WebsocketService {
 | 
			
		||||
    this.isTrackingTx = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackDonation(id: string) {
 | 
			
		||||
    this.websocketSubject.next({ 'track-donation': id });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  stopTrackingTransaction() {
 | 
			
		||||
    if (!this.isTrackingTx) {
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
@ -21,6 +21,11 @@ $pagination-hover-bg:              #12131e;
 | 
			
		||||
$pagination-hover-border-color:     #1d1f31;
 | 
			
		||||
$pagination-disabled-bg:          #1d1f31;
 | 
			
		||||
 | 
			
		||||
.input-group-text {
 | 
			
		||||
  background-color: #1c2031 !important;
 | 
			
		||||
  border: 1px solid #20263e !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
$link-color:                #1bd8f4;
 | 
			
		||||
$link-decoration:           none !default;
 | 
			
		||||
$link-hover-color:          darken($link-color, 15%) !default;
 | 
			
		||||
 | 
			
		||||
@ -84,3 +84,19 @@ 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;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user