From a07a4de2559a6c073e480fbbedc68f57ac51eb86 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 7 Oct 2020 20:15:42 +0700 Subject: [PATCH 1/8] Custom BTCPay donation integration fixes #122 --- backend/mempool-config.sample.json | 4 +- backend/src/api/donations.ts | 125 ++++++++++++++++++ backend/src/api/websocket-handler.ts | 19 +++ backend/src/index.ts | 14 +- backend/src/routes.ts | 74 +++++++++-- .../app/components/about/about.component.html | 44 +++++- .../app/components/about/about.component.scss | 14 ++ .../app/components/about/about.component.ts | 36 ++++- .../src/app/interfaces/websocket.interface.ts | 2 + frontend/src/app/services/api.service.ts | 12 ++ frontend/src/app/services/state.service.ts | 1 + .../src/app/services/websocket.service.ts | 8 ++ frontend/src/styles.scss | 5 + mariadb-structure.sql | 16 +++ 14 files changed, 359 insertions(+), 15 deletions(-) create mode 100644 backend/src/api/donations.ts diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 5c03a7d5e..85be1e664 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -22,5 +22,7 @@ "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_AUTH": "" } diff --git a/backend/src/api/donations.ts b/backend/src/api/donations.ts new file mode 100644 index 000000000..ced8ad31b --- /dev/null +++ b/backend/src/api/donations.ts @@ -0,0 +1,125 @@ +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 { + const postData = { + 'price': amount, + 'orderId': orderId, + 'currency': 'BTC', + 'itemDesc': 'Sponsor mempool.space', + 'notificationUrl': 'https://mempool.space/api/v1/donations-webhook', + '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') { + if (this.notifyDonationStatusCallback) { + this.notifyDonationStatusCallback(data.id); + } + + let imageUrl = ''; + if (response.orderId !== '') { + try { + imageUrl = await this.$getTwitterImageUrl(response.orderId); + } catch (e) { + console.log('Error fetching twitter image from Hive', e.message); + } + } + + this.$addDonationToDatabase(response, imageUrl); + } + } + + private getStatus(id: string): Promise { + 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(query); + connection.release(); + return rows; + } catch (e) { + console.log('$getDonationsFromDatabase() error', e); + } + } + + private async $addDonationToDatabase(response: any, imageUrl: string): Promise { + 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 { + 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(); diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 073c51756..9957356d3 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -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'); diff --git a/backend/src/index.ts b/backend/src/index.ts index 3673a31be..8baac48f0 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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)) + ; + } } } diff --git a/backend/src/routes.ts b/backend/src/routes.ts index ecf827dee..0442fcb7a 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -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.01) { + res.status(400).send('Amount needs to be at least 0.01'); + 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]; } } } diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 549b4fe5a..df1a09dc3 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -12,7 +12,49 @@

- +

❤️ Sponsors

+ +
+

+ + + +
+
+
+
+ +
+ +
+
+
+ @ +
+ +
+
+ +
+
+
+ +
+
+ +
+
+

{{ donationObj.address }}

+

Waiting for transaction...

+
+
+ +
+

Donation confirmed!
Thank you!

+

If you specified a Twitter handle, the profile photo should now be visible on this page when you reload.

+
+ +

GitHub

diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 5e208bcf8..16713b38d 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -14,3 +14,17 @@ tr { .nowrap { white-space: nowrap; } + +.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; +} diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index 4c1494132..86765a336 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -3,22 +3,29 @@ 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'; @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; + donationForm: FormGroup; + donationStatus = 1; + sponsors$: Observable; + donationObj: any; constructor( private websocketService: WebsocketService, private seoService: SeoService, private stateService: StateService, + private formBuilder: FormBuilder, + private apiService: ApiService, ) { } ngOnInit() { @@ -31,5 +38,32 @@ export class AboutComponent implements OnInit { if (document.location.port !== '') { this.hostname = this.hostname + ':' + document.location.port; } + + this.donationForm = this.formBuilder.group({ + amount: [0.001], + 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'); } } diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index bc40e00e1..893dc1f99 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -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 { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index b43f8f6e0..5f97481fb 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -61,4 +61,16 @@ export class ApiService { }); return this.httpClient.get(this.apiBaseUrl + '/transaction-times', { params }); } + + requestDonation$(amount: number, orderId: string): Observable { + const params = { + amount: amount, + orderId: orderId, + }; + return this.httpClient.post(this.apiBaseUrl + '/donations', params); + } + + getDonation$(): Observable { + return this.httpClient.get(this.apiBaseUrl + '/donations'); + } } diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 7c28ca773..d3eb1ea65 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -34,6 +34,7 @@ export class StateService { vbytesPerSecond$ = new ReplaySubject(1); lastDifficultyAdjustment$ = new ReplaySubject(1); gitCommit$ = new ReplaySubject(1); + donationConfirmed$ = new Subject(); live2Chart$ = new Subject(); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index a3d8b9145..d986bd467 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -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; diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 9eb5e36d2..c88ed18f2 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -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; diff --git a/mariadb-structure.sql b/mariadb-structure.sql index 4d567ed91..770f8029a 100644 --- a/mariadb-structure.sql +++ b/mariadb-structure.sql @@ -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; From 17dd03682b923b91199606a967fe2777de933e0f Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 7 Oct 2020 20:19:22 +0700 Subject: [PATCH 2/8] Set 0.01 BTC as minimum donation limit refs #122 --- frontend/src/app/components/about/about.component.html | 2 +- frontend/src/app/components/about/about.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index df1a09dc3..5ebd35307 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -25,7 +25,7 @@
- +
diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index 86765a336..3e9251811 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -40,7 +40,7 @@ export class AboutComponent implements OnInit { } this.donationForm = this.formBuilder.group({ - amount: [0.001], + amount: [0.01], handle: [''], }); From 0ee2753100d2330e89fcc19fd13cc8402341fad4 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 7 Oct 2020 20:27:22 +0700 Subject: [PATCH 3/8] Only display About page on main Bitcoin network. --- frontend/src/app/app-routing.module.ts | 8 -------- .../app/components/master-page/master-page.component.html | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index dce48d646..4daffcaa8 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -98,10 +98,6 @@ const routes: Routes = [ path: 'graphs', component: StatisticsComponent, }, - { - path: 'about', - component: AboutComponent, - }, { path: 'address/:id', component: AddressComponent @@ -167,10 +163,6 @@ const routes: Routes = [ path: 'graphs', component: StatisticsComponent, }, - { - path: 'about', - component: AboutComponent, - }, { path: 'address/:id', children: [], diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 7e704f5e1..bb4d32ac9 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -52,7 +52,7 @@ - From b6738dd9e88afc1bdc8161a71e408a6e97016164 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 7 Oct 2020 23:24:01 +0700 Subject: [PATCH 4/8] Moving API docs to separate /api page. --- frontend/proxy.conf.json | 16 ++-- frontend/src/app/app-routing.module.ts | 13 +++ frontend/src/app/app.module.ts | 5 +- frontend/src/app/bisq/bisq.routing.module.ts | 5 ++ .../app/components/about/about.component.html | 90 +------------------ .../app/components/about/about.component.scss | 21 +---- .../app/components/about/about.component.ts | 8 -- .../api-docs/api-docs.component.html | 88 ++++++++++++++++++ .../api-docs/api-docs.component.scss | 16 ++++ .../components/api-docs/api-docs.component.ts | 26 ++++++ .../master-page/master-page.component.html | 3 + 11 files changed, 168 insertions(+), 123 deletions(-) create mode 100644 frontend/src/app/components/api-docs/api-docs.component.html create mode 100644 frontend/src/app/components/api-docs/api-docs.component.scss create mode 100644 frontend/src/app/components/api-docs/api-docs.component.ts diff --git a/frontend/proxy.conf.json b/frontend/proxy.conf.json index 4e6c6e197..9e99f4b27 100644 --- a/frontend/proxy.conf.json +++ b/frontend/proxy.conf.json @@ -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" } } } \ No newline at end of file diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 4daffcaa8..662a96179 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -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: [], @@ -110,6 +115,10 @@ const routes: Routes = [ path: 'assets', component: AssetsComponent, }, + { + path: 'api', + component: ApiDocsComponent, + }, ], }, { @@ -168,6 +177,10 @@ const routes: Routes = [ children: [], component: AddressComponent }, + { + path: 'api', + component: ApiDocsComponent, + }, ], }, { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 8482e8df1..9997ff97e 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -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); diff --git a/frontend/src/app/bisq/bisq.routing.module.ts b/frontend/src/app/bisq/bisq.routing.module.ts index da0c490c6..90bec63ce 100644 --- a/frontend/src/app/bisq/bisq.routing.module.ts +++ b/frontend/src/app/bisq/bisq.routing.module.ts @@ -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: '' diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 5ebd35307..c31534ac8 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -56,7 +56,7 @@

-

GitHub

+

Open source

@@ -70,98 +70,10 @@ github.com/mempool/mempool
-

- -
-

API

-
- - - -
-
Git commit: {{ gitCommit$ | async }}
-
-
diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 16713b38d..0bb662b1e 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -1,20 +1,3 @@ -.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; -} - .qr-wrapper { background-color: #FFF; padding: 10px; @@ -28,3 +11,7 @@ tr { border-radius: 50%; cursor: pointer; } + +.text-small { + font-size: 12px; +} \ No newline at end of file diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index 3e9251811..8e85e472a 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -12,8 +12,6 @@ import { ApiService } from 'src/app/services/api.service'; styleUrls: ['./about.component.scss'], }) export class AboutComponent implements OnInit { - active = 1; - hostname = document.location.hostname; gitCommit$: Observable; donationForm: FormGroup; donationStatus = 1; @@ -32,12 +30,6 @@ export class AboutComponent implements OnInit { 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], diff --git a/frontend/src/app/components/api-docs/api-docs.component.html b/frontend/src/app/components/api-docs/api-docs.component.html new file mode 100644 index 000000000..b268545db --- /dev/null +++ b/frontend/src/app/components/api-docs/api-docs.component.html @@ -0,0 +1,88 @@ +
+
+

API documentation

+
+ + + +
+ +
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/api-docs/api-docs.component.scss b/frontend/src/app/components/api-docs/api-docs.component.scss new file mode 100644 index 000000000..5e208bcf8 --- /dev/null +++ b/frontend/src/app/components/api-docs/api-docs.component.scss @@ -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; +} diff --git a/frontend/src/app/components/api-docs/api-docs.component.ts b/frontend/src/app/components/api-docs/api-docs.component.ts new file mode 100644 index 000000000..f6fcfe94f --- /dev/null +++ b/frontend/src/app/components/api-docs/api-docs.component.ts @@ -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; + } + } + +} diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index bb4d32ac9..6aa8d4291 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -55,6 +55,9 @@ + From 5b8dbfca7419aca8a2854242c39364790651570f Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 7 Oct 2020 23:30:45 +0700 Subject: [PATCH 5/8] Addig fronend flag (SPONSORS_ENABLED) to enable Sponsors in the gui. refs #122 --- frontend/mempool-frontend-config.sample.json | 3 +- frontend/src/app/app.constants.ts | 2 + .../app/components/about/about.component.html | 76 ++++++++++--------- .../app/components/about/about.component.ts | 2 + 4 files changed, 46 insertions(+), 37 deletions(-) diff --git a/frontend/mempool-frontend-config.sample.json b/frontend/mempool-frontend-config.sample.json index 0312fd054..a5fe5c55e 100644 --- a/frontend/mempool-frontend-config.sample.json +++ b/frontend/mempool-frontend-config.sample.json @@ -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 } \ No newline at end of file diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index fb493de5a..7e817d714 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -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 }; diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index c31534ac8..a7a289b20 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -12,49 +12,53 @@

-

❤️ Sponsors

+ -
-

- - +

❤️ Sponsors

-
-
-
-
- +
+

+ + + +
+ +
+
+ +
+
- -
-
-
- @ +
+
+ @ +
+
- -
-
- -
- -
- -
-
- +
+ +
+
-
-

{{ donationObj.address }}

-

Waiting for transaction...

-
-
-
-

Donation confirmed!
Thank you!

-

If you specified a Twitter handle, the profile photo should now be visible on this page when you reload.

-
+
+
+ +
+
+

{{ donationObj.address }}

+

Waiting for transaction...

+
+
-

+
+

Donation confirmed!
Thank you!

+

If you specified a Twitter handle, the profile photo should now be visible on this page when you reload.

+
+ +

+ +

Open source

diff --git a/frontend/src/app/components/about/about.component.ts b/frontend/src/app/components/about/about.component.ts index 8e85e472a..fbdeb73dc 100644 --- a/frontend/src/app/components/about/about.component.ts +++ b/frontend/src/app/components/about/about.component.ts @@ -5,6 +5,7 @@ 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', @@ -17,6 +18,7 @@ export class AboutComponent implements OnInit { donationStatus = 1; sponsors$: Observable; donationObj: any; + sponsorsEnabled = env.SPONSORS_ENABLED; constructor( private websocketService: WebsocketService, From 0cbc7e2ab65e902426bf1e82c00bea57d6dfd282 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 8 Oct 2020 00:15:26 +0700 Subject: [PATCH 6/8] Make BTCPay webhook configurable. refs #122 --- backend/mempool-config.sample.json | 1 + backend/src/api/donations.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index 85be1e664..018c079d2 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -24,5 +24,6 @@ "SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem", "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem", "BTCPAY_URL": "", + "BTCPAY_WEBHOOK_URL": "", "BTCPAY_AUTH": "" } diff --git a/backend/src/api/donations.ts b/backend/src/api/donations.ts index ced8ad31b..1c13f7a79 100644 --- a/backend/src/api/donations.ts +++ b/backend/src/api/donations.ts @@ -24,7 +24,7 @@ class Donations { 'orderId': orderId, 'currency': 'BTC', 'itemDesc': 'Sponsor mempool.space', - 'notificationUrl': 'https://mempool.space/api/v1/donations-webhook', + 'notificationUrl': config.BTCPAY_WEBHOOK_URL, 'redirectURL': 'https://mempool.space/about' }; return new Promise((resolve, reject) => { From 1f7483687fdcd82c472cfdecf983ef5b8c9c3472 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 8 Oct 2020 00:20:42 +0700 Subject: [PATCH 7/8] Don't allow invoices lower than 0.001 and require 0.01 for sponsorship. refs #122 --- backend/src/api/donations.ts | 37 +++++++++++-------- backend/src/routes.ts | 4 +- .../app/components/about/about.component.html | 14 +++++-- 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/backend/src/api/donations.ts b/backend/src/api/donations.ts index 1c13f7a79..fde21940f 100644 --- a/backend/src/api/donations.ts +++ b/backend/src/api/donations.ts @@ -48,23 +48,30 @@ class Donations { if (!data || !data.id) { return; } + const response = await this.getStatus(data.id); - if (response.status === 'complete') { - if (this.notifyDonationStatusCallback) { - this.notifyDonationStatusCallback(data.id); - } - - let imageUrl = ''; - if (response.orderId !== '') { - try { - imageUrl = await this.$getTwitterImageUrl(response.orderId); - } catch (e) { - console.log('Error fetching twitter image from Hive', e.message); - } - } - - this.$addDonationToDatabase(response, imageUrl); + if (response.status !== 'complete') { + 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 { diff --git a/backend/src/routes.ts b/backend/src/routes.ts index 0442fcb7a..cde97e39c 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -117,8 +117,8 @@ class Routes { return; } - if (p.amount < 0.01) { - res.status(400).send('Amount needs to be at least 0.01'); + if (p.amount < 0.001) { + res.status(400).send('Amount needs to be at least 0.001'); return; } diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index a7a289b20..f84270ca1 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -27,9 +27,9 @@
- +
-
+
@
@@ -41,12 +41,20 @@
+ +
+ If you donate 0.01 BTC or more, your profile photo will be added to the list of sponsors above :) +
+
+
-
+

{{ donationObj.address }}

+

{{ donationObj.amount }} BTC

+

Waiting for transaction...

From 87dc1e5db4261a63fcde4d386bc4d6f07bf2d732 Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 8 Oct 2020 00:51:11 +0700 Subject: [PATCH 8/8] Check and accept both 'complete' and 'confirmed' invoices. refs #122 --- backend/src/api/donations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/donations.ts b/backend/src/api/donations.ts index fde21940f..8a18b6cb4 100644 --- a/backend/src/api/donations.ts +++ b/backend/src/api/donations.ts @@ -50,7 +50,7 @@ class Donations { } const response = await this.getStatus(data.id); - if (response.status !== 'complete') { + if (response.status !== 'complete' && response.status !== 'confirmed') { return; }