Merge branch 'master' into nymkappa/prepaid-acceleration

This commit is contained in:
Felipe Knorr Kuhn
2024-04-02 21:53:01 +09:00
committed by GitHub
307 changed files with 13605 additions and 17525 deletions

View File

@@ -19,6 +19,7 @@
"@typescript-eslint/no-this-alias": 1,
"@typescript-eslint/no-var-requires": 1,
"@typescript-eslint/explicit-function-return-type": 1,
"@typescript-eslint/no-unused-vars": 1,
"no-case-declarations": 1,
"no-console": 1,
"no-constant-condition": 1,

View File

@@ -22,14 +22,13 @@ cd mempool/frontend
### 2. Specify Website
The same frontend codebase is used for https://mempool.space, https://liquid.network and https://bisq.markets.
The same frontend codebase is used for https://mempool.space and https://liquid.network.
Configure the frontend for the site you want by running the corresponding command:
```
$ npm run config:defaults:mempool
$ npm run config:defaults:liquid
$ npm run config:defaults:bisq
```
### 3. Run the Frontend

View File

@@ -223,11 +223,11 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "mempool:build"
"buildTarget": "mempool:build"
},
"configurations": {
"production": {
"browserTarget": "mempool:build:production"
"buildTarget": "mempool:build:production"
},
"local": {
"proxyConfig": "proxy.conf.local.js",
@@ -264,7 +264,7 @@
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "mempool:build"
"buildTarget": "mempool:build"
}
},
"e2e": {
@@ -280,6 +280,56 @@
}
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/mempool/server",
"main": "server.ts",
"tsConfig": "tsconfig.server.json"
},
"configurations": {
"production": {
"outputHashing": "media",
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"sourceMap": false,
"localize": true,
"optimization": false
}
}
},
"serve-ssr": {
"builder": "@angular-devkit/build-angular:ssr-dev-server",
"options": {
"browserTarget": "mempool:build",
"serverTarget": "mempool:server"
},
"configurations": {
"production": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production",
"optimization": false,
"sourceMap": true
}
}
},
"prerender": {
"builder": "@angular-devkit/build-angular:prerender",
"options": {
"browserTarget": "mempool:build:production",
"serverTarget": "mempool:server:production",
"routes": [
"/"
]
},
"configurations": {
"production": {}
}
},
"cypress-run": {
"builder": "@cypress/schematic:cypress",
"options": {

View File

@@ -1,4 +1,4 @@
describe.skip('Liquid', () => {
describe('Liquid', () => {
const baseModule = Cypress.env('BASE_MODULE');
const basePath = '';
@@ -23,6 +23,13 @@ describe.skip('Liquid', () => {
cy.get('#mempool-block-0 > .blockLink').should('exist');
});
it('load first mempool block after skeleton loads', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.get('#mempool-block-0 > .blockLink').click();
cy.waitForSkeletonGone();
});
it('loads the dashboard', () => {
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
@@ -84,10 +91,11 @@ describe.skip('Liquid', () => {
cy.waitForSkeletonGone();
//TODO: Change to an element id so we don't assert on a string
cy.get('.table-tx-vin').should('contain', 'Peg-in');
cy.get('.table-tx-vin a').click().then(() => {
//Remove the target=_blank attribute so the new url opens in the same tab
cy.get('.table-tx-vin a').invoke('removeAttr', 'target').click().then(() => {
cy.waitForSkeletonGone();
if (baseModule === 'liquid') {
cy.url().should('eq', 'https://mempool.space/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592');
cy.url().should('eq', 'https://mempool.space/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592#vout=0');
} else {
//TODO: Use an environment variable to get the hostname
cy.url().should('eq', 'http://localhost:4200/tx/f148c0d854db4174ea420655235f910543f0ec3680566dcfdf84fb0a1697b592');
@@ -98,7 +106,8 @@ describe.skip('Liquid', () => {
it('loads peg out addresses', () => {
cy.visit(`${basePath}/tx/ecf6eba04ffb3946faa172343c87162df76f1a57b07b0d6dc6ad956b13376dc8`);
cy.waitForSkeletonGone();
cy.get('.table-tx-vout a').first().click().then(() => {
//Remove the target=_blank attribute so the new url opens in the same tab
cy.get('.table-tx-vout a').first().invoke('removeAttr', 'target').click().then(() => {
cy.waitForSkeletonGone();
if (baseModule === 'liquid') {
cy.url().should('eq', 'https://mempool.space/address/1BxoGcMg14oaH3CwHD2hF4gU9VcfgX5yoR');

View File

@@ -1,4 +1,4 @@
describe.skip('Liquid Testnet', () => {
describe('Liquid Testnet', () => {
const baseModule = Cypress.env('BASE_MODULE');
const basePath = '/testnet';
@@ -28,6 +28,17 @@ describe.skip('Liquid Testnet', () => {
cy.waitForSkeletonGone();
});
it.skip('loads the dashboard with no scrollbars on mobile', () => {
cy.viewport('iphone-xr');
cy.visit(`${basePath}`);
cy.waitForSkeletonGone();
cy.window().then(window => {
const htmlWidth = Cypress.$('html')[0].scrollWidth;
const scrollBarWidth = window.innerWidth - htmlWidth;
expect(scrollBarWidth).to.be.eq(0); //check for no horizontal scrollbar
});
});
it('loads the blocks page', () => {
cy.visit(`${basePath}`)
cy.get('#btn-blocks');
@@ -57,17 +68,14 @@ describe.skip('Liquid Testnet', () => {
cy.get('.tv-only').should('not.exist');
});
it.skip('renders unconfidential addresses correctly on mobile', () => {
cy.viewport('iphone-6');
cy.visit(`${basePath}/address/__TODO__`);
it.skip('renders unconfidential transactions correctly on mobile', () => {
cy.viewport('iphone-xr');
cy.visit(`${basePath}/tx/b119f338878416781dc285b94c0de52826341dea43566e4de4740d3ebfd1f6dc#blinded=99707,144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49,1377e4ec8eb0c89296e14ffca57e377f4b51ad8f1c881e43364434d8430dbfda,cdd6caae4c3452586cfcb107478dd2b7acaa5f82714a6a966578255e857eee60`);
cy.waitForSkeletonGone();
//TODO: Add proper IDs for these selectors
const firstRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(1) > .table > tbody';
const thirdRowSelector = '.container-xl > :nth-child(3) > div > :nth-child(3)';
cy.get(firstRowSelector).invoke('css', 'width').then(firstRowWidth => {
cy.get(thirdRowSelector).invoke('css', 'width').then(thirdRowWidth => {
expect(parseInt(firstRowWidth)).to.be.lessThan(parseInt(thirdRowWidth));
});
cy.window().then(window => {
const htmlWidth = Cypress.$('html')[0].scrollWidth;
const scrollBarWidth = window.innerWidth - htmlWidth;
expect(scrollBarWidth).to.be.eq(0); //check for no horizontal scrollbar
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -38,7 +38,13 @@ export const mockWebSocket = () => {
win.mockServer = server;
win.mockServer.on('connection', (socket) => {
win.mockSocket = socket;
win.mockSocket.send('{"action":"init"}');
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
cy.readFile('cypress/fixtures/mainnet_mempoolInfo.json', 'ascii').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));
});
});
win.mockServer.on('message', (message) => {
@@ -75,8 +81,6 @@ export const emitMempoolInfo = ({
switch (params.command) {
case "init": {
win.mockSocket.send('{"action":"init"}');
win.mockSocket.send('{"action":"want","data":["blocks","stats","mempool-blocks","live-2h-chart"]}');
win.mockSocket.send('{"conversions":{"USD":32365.338815782445}}');
cy.readFile('cypress/fixtures/mainnet_live2hchart.json', 'ascii').then((fixture) => {
win.mockSocket.send(JSON.stringify(fixture));

View File

@@ -71,7 +71,7 @@ const newConfig = `(function (window) {
window.__env.${obj.key} = ${typeof obj.value === 'string' ? `'${obj.value}'` : obj.value};`, '')}
window.__env.GIT_COMMIT_HASH = '${gitCommitHash}';
window.__env.PACKAGE_JSON_VERSION = '${packetJsonVersion}';
}(this));`;
}((typeof global !== 'undefined') ? global : this));`;
const newConfigTemplate = `(function (window) {
window.__env = window.__env || {};${settings.reduce((str, obj) => `${str}

View File

@@ -3,8 +3,6 @@
"SIGNET_ENABLED": false,
"LIQUID_ENABLED": false,
"LIQUID_TESTNET_ENABLED": false,
"BISQ_ENABLED": false,
"BISQ_SEPARATE_BACKEND": false,
"ITEMS_PER_PAGE": 10,
"KEEP_BLOCKS_AMOUNT": 8,
"NGINX_PROTOCOL": "http",
@@ -15,7 +13,6 @@
"BASE_MODULE": "mempool",
"MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets",
"MINING_DASHBOARD": true,
"AUDIT": false,
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,

12090
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,13 +34,12 @@
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
"start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
"start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
"build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets-dev && npm run sync-assets && npm run build-mempool.js",
"sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js 'dist/mempool/browser/resources/'",
"sync-assets-dev": "node sync-assets.js 'src/resources/'",
"generate-config": "node generate-config.js",
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
"build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js",
"build-mempool-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index.js --standalone mempoolJS > ./dist/mempool/browser/en-US/mempool.js",
"build-mempool-bisq-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-bisq.js --standalone bisqJS > ./dist/mempool/browser/en-US/bisq.js",
"build-mempool-liquid-js": "browserify -p tinyify ./node_modules/@mempool/mempool.js/lib/index-liquid.js --standalone liquidJS > ./dist/mempool/browser/en-US/liquid.js",
"test": "npm run ng -- test",
"lint": "./node_modules/.bin/eslint . --ext .ts",
@@ -48,37 +47,40 @@
"prettier": "prettier --write \"src/app/**/*.{js,json,css,scss,less,md,ts,html,component.html}\"",
"e2e": "npm run generate-config && npm run ng -- e2e",
"e2e:ci": "npm run cypress:run:ci",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
"config:defaults:bisq": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=bisq BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"dev:ssr": "npm run generate-config && ng run mempool:serve-ssr",
"serve:ssr": "npm run generate-config && node server.run.js",
"build:ssr": "npm run build && ng run mempool:server:production && ./node_modules/typescript/bin/tsc server.run.ts",
"config:defaults:mempool": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=mempool BLOCK_WEIGHT_UNITS=4000000 && npm run generate-config",
"config:defaults:liquid": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true LIQUID_TESTNET_ENABLED=true ITEMS_PER_PAGE=25 BASE_MODULE=liquid BLOCK_WEIGHT_UNITS=300000 && npm run generate-config",
"prerender": "npm run ng -- run mempool:prerender",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:record": "cypress run --record",
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true BISQ_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
"cypress:open:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:open",
"cypress:run:ci": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-prod 4200 cypress:run:record",
"cypress:open:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:open",
"cypress:run:ci:staging": "node update-config.js TESTNET_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:local-staging 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^16.2.0",
"@angular/animations": "^16.2.2",
"@angular/cli": "^16.2.0",
"@angular/common": "^16.2.2",
"@angular/compiler": "^16.2.2",
"@angular/core": "^16.2.2",
"@angular/forms": "^16.2.2",
"@angular/localize": "^16.2.2",
"@angular/platform-browser": "^16.2.2",
"@angular/platform-browser-dynamic": "^16.2.2",
"@angular/platform-server": "^16.2.2",
"@angular/router": "^16.2.2",
"@fortawesome/angular-fontawesome": "~0.13.0",
"@angular-devkit/build-angular": "^17.3.1",
"@angular/animations": "^17.3.1",
"@angular/cli": "^17.3.1",
"@angular/common": "^17.3.1",
"@angular/compiler": "^17.3.1",
"@angular/core": "^17.3.1",
"@angular/forms": "^17.3.1",
"@angular/localize": "^17.3.1",
"@angular/platform-browser": "^17.3.1",
"@angular/platform-browser-dynamic": "^17.3.1",
"@angular/platform-server": "^17.3.1",
"@angular/router": "^17.3.1",
"@angular/ssr": "^17.3.1",
"@fortawesome/angular-fontawesome": "~0.14.1",
"@fortawesome/fontawesome-common-types": "~6.5.1",
"@fortawesome/fontawesome-svg-core": "~6.5.1",
"@fortawesome/free-solid-svg-icons": "~6.5.1",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^15.1.0",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@types/qrcode": "~1.5.0",
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
@@ -86,26 +88,29 @@
"domino": "^2.1.6",
"echarts": "~5.5.0",
"lightweight-charts": "~3.8.0",
"ngx-echarts": "~16.2.0",
"ngx-infinite-scroll": "^16.0.0",
"ngx-echarts": "~17.1.0",
"ngx-infinite-scroll": "^17.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"tinyify": "^3.1.0",
"esbuild": "^0.20.2",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.6.0",
"zone.js": "~0.13.1"
"zone.js": "~0.14.4"
},
"devDependencies": {
"@angular/compiler-cli": "^16.1.5",
"@angular/language-service": "^16.1.5",
"@angular/compiler-cli": "^17.3.1",
"@angular/language-service": "^17.3.1",
"@types/node": "^18.11.9",
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.31.0",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"eslint": "^8.57.0",
"browser-sync": "^3.0.0",
"http-proxy-middleware": "~2.0.6",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"ts-node": "~10.9.1",
"typescript": "~4.9.3"
"typescript": "~5.4.3"
},
"optionalDependencies": {
"@cypress/schematic": "^2.5.0",

View File

@@ -22,7 +22,6 @@ PROXY_CONFIG = [
{
context: ['*',
'/api/**', '!/api/v1/ws',
'!/bisq', '!/bisq/**', '!/bisq/',
'!/liquid', '!/liquid/**', '!/liquid/',
'!/liquidtestnet', '!/liquidtestnet/**', '!/liquidtestnet/',
'/testnet/api/**', '/signet/api/**'
@@ -39,16 +38,6 @@ PROXY_CONFIG = [
secure: false,
changeOrigin: true,
},
{
context: ['/api/bisq**', '/bisq/api/**'],
target: "https://bisq.markets",
pathRewrite: {
"^/api/bisq/": "/bisq/api"
},
ws: true,
secure: false,
changeOrigin: true
},
{
context: ['/api/liquid**', '/liquid/api/**'],
target: "https://liquid.network",

View File

@@ -67,40 +67,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
]);
}
if (configContent && configContent.BASE_MODULE === 'bisq') {
PROXY_CONFIG.push(...[
{
context: ['/bisq/api/v1/ws'],
target: `http://127.0.0.1:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq": ""
},
},
{
context: ['/bisq/api/v1/**'],
target: `http://127.0.0.1:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/bisq/api/**'],
target: `http://127.0.0.1:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq/api/": "/api/v1/bisq/"
},
}
]);
}
PROXY_CONFIG.push(...[
{
context: ['/testnet/api/v1/lightning/**'],

View File

@@ -67,40 +67,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
]);
}
if (configContent && configContent.BASE_MODULE === 'bisq') {
PROXY_CONFIG.push(...[
{
context: ['/bisq/api/v1/ws'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq": ""
},
},
{
context: ['/bisq/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/bisq/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq/api/": "/api/v1/bisq/"
},
}
]);
}
PROXY_CONFIG.push(...[
{
context: ['/testnet/api/v1/lightning/**'],

View File

@@ -61,39 +61,6 @@ if (configContent && configContent.BASE_MODULE === 'liquid') {
]);
}
if (configContent && configContent.BASE_MODULE === 'bisq') {
PROXY_CONFIG.push(...[
{
context: ['/bisq/api/v1/ws'],
target: `http://localhost:8999`,
secure: false,
ws: true,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq": ""
},
},
{
context: ['/bisq/api/v1/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
},
{
context: ['/bisq/api/**'],
target: `http://localhost:8999`,
secure: false,
changeOrigin: true,
proxyTimeout: 30000,
pathRewrite: {
"^/bisq/api/": "/api/v1/bisq/"
},
},
]);
}
PROXY_CONFIG.push(...[
{
context: ['/api/v1/services/**'],

View File

@@ -5,7 +5,6 @@ let PROXY_CONFIG = require('./proxy.conf');
PROXY_CONFIG.forEach(entry => {
entry.target = entry.target.replace("mempool.space", "mempool-staging.fra.mempool.space");
entry.target = entry.target.replace("liquid.network", "liquid-staging.fra.mempool.space");
entry.target = entry.target.replace("bisq.markets", "bisq-staging.fra.mempool.space");
});
module.exports = PROXY_CONFIG;

104
frontend/server.run.ts Normal file
View File

@@ -0,0 +1,104 @@
import './src/resources/config.js';
import * as domino from 'domino';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
const {readFileSync, existsSync} = require('fs');
const {createProxyMiddleware} = require('http-proxy-middleware');
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = () => {
return {
matches: true
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
// @ts-ignore
global['window'] = win;
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
/**
* Return the list of supported and actually active locales
*/
function getActiveLocales() {
const angularConfig = JSON.parse(readFileSync('angular.json', 'utf8'));
const supportedLocales = [
angularConfig.projects.mempool.i18n.sourceLocale,
...Object.keys(angularConfig.projects.mempool.i18n.locales),
];
return supportedLocales.filter(locale => locale === 'en-US' && existsSync(`./dist/mempool/server/${locale}`));
// return supportedLocales.filter(locale => existsSync(`./dist/mempool/server/${locale}`));
}
function app() {
const server = express();
// proxy websocket
server.get('/api/v1/ws', createProxyMiddleware({
target: 'ws://localhost:4200/api/v1/ws',
changeOrigin: true,
ws: true,
logLevel: 'debug'
}));
// proxy API to nginx
server.get('/api/**', createProxyMiddleware({
// @ts-ignore
target: win.__env.NGINX_PROTOCOL + '://' + win.__env.NGINX_HOSTNAME + ':' + win.__env.NGINX_PORT,
changeOrigin: true,
}));
server.get('/resources/**', express.static('./src'));
// map / and /en to en-US
const defaultLocale = 'en-US';
console.log(`serving default locale: ${defaultLocale}`);
const appServerModule = require(`./dist/mempool/server/${defaultLocale}/main.js`);
server.use('/', appServerModule.app(defaultLocale));
server.use('/en', appServerModule.app(defaultLocale));
// map each locale to its localized main.js
getActiveLocales().forEach(locale => {
console.log('serving locale:', locale);
const appServerModule = require(`./dist/mempool/server/${locale}/main.js`);
// map everything to itself
server.use(`/${locale}`, appServerModule.app(locale));
});
return server;
}
function run() {
const port = process.env.PORT || 4000;
// Start up the Node server
app().listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
run();

108
frontend/server.ts Normal file
View File

@@ -0,0 +1,108 @@
import 'zone.js';
import './src/resources/config.js';
import { CommonEngine } from '@angular/ssr';
import * as express from 'express';
import * as fs from 'fs';
import * as path from 'path';
import * as domino from 'domino';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';
import { ResizeObserver } from './shims';
const commonEngine = new CommonEngine();
const template = fs.readFileSync(path.join(process.cwd(), 'dist/mempool/browser/en-US/', 'index.html')).toString();
const win = domino.createWindow(template);
// @ts-ignore
win.__env = global.__env;
// @ts-ignore
win.matchMedia = (media) => {
return {
media,
matches: true,
};
};
// @ts-ignore
win.setTimeout = (fn) => { fn(); };
win.document.body.scrollTo = (() => {});
win['ResizeObserver'] = ResizeObserver;
// @ts-ignore
global['window'] = win;
// @ts-ignore
global['document'] = win.document;
// @ts-ignore
global['history'] = { state: { } };
// @ts-ignore
Object.defineProperty(global, 'navigator', {
value: win.navigator,
writable: true
});
global['localStorage'] = {
getItem: () => '',
setItem: () => {},
removeItem: () => {},
clear: () => {},
length: 0,
key: () => '',
};
// The Express app is exported so that it can be used by serverless Functions.
export function app(locale: string): express.Express {
const server = express();
const distFolder = join(process.cwd(), `dist/mempool/browser/${locale}`);
const indexHtml = join(distFolder, 'index.html');
server.set('view engine', 'html');
server.set('views', distFolder);
// static file handler so we send HTTP 404 to nginx
server.get('/**.(css|js|json|ico|webmanifest|png|jpg|jpeg|svg|mp4)*', express.static(distFolder, { maxAge: '1y', fallthrough: false }));
// handle page routes
server.get('*', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
commonEngine
.render({
bootstrap: AppServerModule,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
publicPath: distFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})
.then((html) => res.send(html))
.catch((err) => next(err));
});
return server;
}
// only used for development mode
function run(): void {
const port = process.env.PORT || 4000;
// Start up the Node server
const server = app('en-US');
server.listen(port, () => {
console.log(`Node Express server listening on port ${port}`);
});
}
// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
run();
}

7
frontend/shims.ts Normal file
View File

@@ -0,0 +1,7 @@
export class ResizeObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
}

View File

@@ -170,13 +170,6 @@ let routes: Routes = [
},
];
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'bisq') {
routes = [{
path: '',
loadChildren: () => import('./bisq/bisq.module').then(m => m.BisqModule)
}];
}
if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
routes = [
{

View File

@@ -1,20 +1,23 @@
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { ServerModule } from '@angular/platform-server';
import { ZONE_SERVICE } from './injection-tokens';
import { AppModule } from './app.module';
import { AppComponent } from './components/app/app.component';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
import { ZoneService } from './services/zone.service';
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule,
],
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
{ provide: ZONE_SERVICE, useClass: ZoneService },
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
export class AppServerModule {}

View File

@@ -2,6 +2,7 @@ import { BrowserModule } from '@angular/platform-browser';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ZONE_SERVICE } from './injection-tokens';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './components/app/app.component';
import { ElectrsApiService } from './services/electrs-api.service';
@@ -13,6 +14,7 @@ import { WebsocketService } from './services/websocket.service';
import { AudioService } from './services/audio.service';
import { SeoService } from './services/seo.service';
import { OpenGraphService } from './services/opengraph.service';
import { ZoneService } from './services/zone-shim.service';
import { SharedModule } from './shared/shared.module';
import { StorageService } from './services/storage.service';
import { HttpCacheInterceptor } from './services/http-cache.interceptor';
@@ -42,7 +44,8 @@ const providers = [
CapAddressPipe,
AppPreloadingStrategy,
ServicesApiServices,
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true }
{ provide: HTTP_INTERCEPTORS, useClass: HttpCacheInterceptor, multi: true },
{ provide: ZONE_SERVICE, useClass: ZoneService },
];
@NgModule({

View File

@@ -1,110 +0,0 @@
<div class="container-xl">
<h1 i18n="shared.address">Address</h1>
<span class="address-link">
<app-truncate [text]="addressString" [lastChars]="8" [link]="['/address/' | relativeUrl, addressString]">
<app-clipboard [text]="addressString"></app-clipboard>
</app-truncate>
</span>
<br>
<div class="clearfix"></div>
<ng-template [ngIf]="!isLoadingAddress && !error">
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="address.total-received">Total received</td>
<td>{{ totalReceived / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="address.total-sent">Total sent</td>
<td>{{ totalSent / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="address.balance">Balance</td>
<td>{{ (totalReceived - totalSent) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="totalReceived - totalSent" [forceFiat]="true" [green]="true"></app-bsq-amount></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col-md qrcode-col">
<div class="qr-wrapper">
<app-qrcode [data]="addressString"></app-qrcode>
</div>
</div>
</div>
</div>
<br>
<h2>
<ng-container *ngTemplateOutlet="transactions.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: transactions.length}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</h2>
<ng-template ngFor let-tx [ngForOf]="transactions">
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
</a>
<div class="float-right">
&lrm;{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
</div>
<div class="clearfix"></div>
</div>
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
<br>
</ng-template>
</ng-template>
<ng-template [ngIf]="isLoadingAddress && !error">
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td colspan="2"><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col">
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
Error loading address data.
<br>
<i>{{ error.error }}</i>
</div>
</ng-template>
</div>
<br>

View File

@@ -1,75 +0,0 @@
.qr-wrapper {
background-color: #FFF;
padding: 10px;
padding-bottom: 5px;
display: inline-block;
}
.qrcode-col {
text-align: center;
}
.qrcode-col > div {
margin: 20px auto 5px;
@media (min-width: 768px) {
text-align: center;
margin: auto;
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 768px) {
display: inline-block;
font-size: 14px;
margin-left: 10px;
}
}
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 768px) {
text-align: left;
}
}
}
}
h1 {
margin: 0px;
padding: 0px;
@media (min-width: 576px) {
float: left;
margin-right: 10px;
}
}
.address-link {
line-height: 26px;
margin-left: 0px;
top: 14px;
position: relative;
display: flex;
flex-direction: row;
@media (min-width: 768px) {
line-height: 38px;
}
}
.row{
flex-direction: column;
@media (min-width: 576px) {
flex-direction: row;
}
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.details-table td:first-child {
white-space: pre-wrap;
}
}

View File

@@ -1,89 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SeoService } from '../../services/seo.service';
import { switchMap, filter, catchError } from 'rxjs/operators';
import { ParamMap, ActivatedRoute } from '@angular/router';
import { Subscription, of } from 'rxjs';
import { BisqTransaction } from '../bisq.interfaces';
import { BisqApiService } from '../bisq-api.service';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-address',
templateUrl: './bisq-address.component.html',
styleUrls: ['./bisq-address.component.scss']
})
export class BisqAddressComponent implements OnInit, OnDestroy {
transactions: BisqTransaction[];
addressString: string;
isLoadingAddress = true;
error: any;
mainSubscription: Subscription;
totalReceived = 0;
totalSent = 0;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private seoService: SeoService,
private bisqApiService: BisqApiService,
) { }
ngOnInit() {
this.websocketService.want(['blocks']);
this.mainSubscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
this.error = undefined;
this.isLoadingAddress = true;
this.transactions = null;
document.body.scrollTo(0, 0);
this.addressString = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq-address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.address:See current balance, pending transactions, and history of confirmed transactions for BSQ address ${this.addressString}:INTERPOLATION:.`);
return this.bisqApiService.getAddress$(this.addressString)
.pipe(
catchError((err) => {
this.isLoadingAddress = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of(null);
})
);
}),
filter((transactions) => transactions !== null)
)
.subscribe((transactions: BisqTransaction[]) => {
this.transactions = transactions;
this.updateChainStats();
this.isLoadingAddress = false;
},
(error) => {
console.log(error);
this.error = error;
this.seoService.logSoft404();
this.isLoadingAddress = false;
});
}
updateChainStats() {
const shortenedAddress = this.addressString.substr(1);
this.totalSent = this.transactions.reduce((acc, tx) =>
acc + tx.inputs
.filter((input) => input.address === shortenedAddress)
.reduce((a, input) => a + input.bsqAmount, 0), 0);
this.totalReceived = this.transactions.reduce((acc, tx) =>
acc + tx.outputs
.filter((output) => output.address === shortenedAddress)
.reduce((a, output) => a + output.bsqAmount, 0), 0);
}
ngOnDestroy() {
this.mainSubscription.unsubscribe();
}
}

View File

@@ -1,78 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpResponse, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { BisqTransaction, BisqBlock, BisqStats, MarketVolume, Trade, Markets, Tickers, Offers, Currencies, HighLowOpenClose, SummarizedInterval } from './bisq.interfaces';
const API_BASE_URL = '/bisq/api';
@Injectable({
providedIn: 'root'
})
export class BisqApiService {
apiBaseUrl: string;
constructor(
private httpClient: HttpClient,
) { }
getStats$(): Observable<BisqStats> {
return this.httpClient.get<BisqStats>(API_BASE_URL + '/stats');
}
getTransaction$(txId: string): Observable<BisqTransaction> {
return this.httpClient.get<BisqTransaction>(API_BASE_URL + '/tx/' + txId);
}
listTransactions$(start: number, length: number, types: string[]): Observable<HttpResponse<BisqTransaction[]>> {
let params = new HttpParams();
types.forEach((t: string) => {
params = params.append('types[]', t);
});
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + `/txs/${start}/${length}`, { params, observe: 'response' });
}
getBlock$(hash: string): Observable<BisqBlock> {
return this.httpClient.get<BisqBlock>(API_BASE_URL + '/block/' + hash);
}
listBlocks$(start: number, length: number): Observable<HttpResponse<BisqBlock[]>> {
return this.httpClient.get<BisqBlock[]>(API_BASE_URL + `/blocks/${start}/${length}`, { observe: 'response' });
}
getAddress$(address: string): Observable<BisqTransaction[]> {
return this.httpClient.get<BisqTransaction[]>(API_BASE_URL + '/address/' + address);
}
getMarkets$(): Observable<Markets> {
return this.httpClient.get<Markets>(API_BASE_URL + '/markets/markets');
}
getMarketsTicker$(): Observable<Tickers> {
return this.httpClient.get<Tickers>(API_BASE_URL + '/markets/ticker');
}
getMarketsCurrencies$(): Observable<Currencies> {
return this.httpClient.get<Currencies>(API_BASE_URL + '/markets/currencies');
}
getMarketsHloc$(market: string, interval: 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day'
| 'week' | 'month' | 'year' | 'auto'): Observable<SummarizedInterval[]> {
return this.httpClient.get<SummarizedInterval[]>(API_BASE_URL + '/markets/hloc?market=' + market + '&interval=' + interval);
}
getMarketOffers$(market: string): Observable<Offers> {
return this.httpClient.get<Offers>(API_BASE_URL + '/markets/offers?market=' + market);
}
getMarketTrades$(market: string): Observable<Trade[]> {
return this.httpClient.get<Trade[]>(API_BASE_URL + '/markets/trades?market=' + market);
}
getMarketVolumesByTime$(period: string): Observable<HighLowOpenClose[]> {
return this.httpClient.get<HighLowOpenClose[]>(API_BASE_URL + '/markets/volumes/' + period);
}
getAllVolumesDay$(): Observable<MarketVolume[]> {
return this.httpClient.get<MarketVolume[]>(API_BASE_URL + '/markets/volumes?interval=week');
}
}

View File

@@ -1,114 +0,0 @@
<div class="container-xl">
<div class="title-block">
<h1><ng-template [ngIf]="blockHeight" i18n="shared.block-title">Block <ng-container *ngTemplateOutlet="blockTemplateContent"></ng-container></ng-template></h1>
</div>
<ng-template #blockTemplateContent><a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a></ng-template>
<div class="clearfix"></div>
<ng-template [ngIf]="!isLoading && !error">
<div class="box block-container">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" title="{{ block.hash }}">{{ block.hash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.hash"></app-clipboard></td>
</tr>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ block.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time>)</i>
</div>
</td>
</tr>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
<td><a [routerLink]="['/block/' | relativeUrl, block.previousBlockHash]" title="{{ block.hash }}">{{ block.previousBlockHash | shortenString : 13 }}</a> <app-clipboard class="d-none d-sm-inline-block" [text]="block.previousBlockHash"></app-clipboard></td>
</tr>
</table>
</div>
</div>
</div>
<div class="clearfix"></div>
<br>
<h2>
<ng-container *ngTemplateOutlet="block.txs.length === 1 ? transactionsSingular : transactionsPlural; context: {$implicit: block.txs.length| number}"></ng-container>
<ng-template #transactionsSingular let-i i18n="shared.transaction-count.singular">{{ i }} transaction</ng-template>
<ng-template #transactionsPlural let-i i18n="shared.transaction-count.plural">{{ i }} transactions</ng-template>
</h2>
<ng-template ngFor let-tx [ngForOf]="block.txs">
<div class="header-bg box" style="padding: 10px; margin-bottom: 10px;">
<a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">
<span style="float: left;" class="d-block d-md-none">{{ tx.id | shortenString : 16 }}</span>
<span style="float: left;" class="d-none d-md-block">{{ tx.id }}</span>
</a>
<div class="float-right">
&lrm;{{ tx.time | date:'yyyy-MM-dd HH:mm' }}
</div>
<div class="clearfix"></div>
</div>
<app-bisq-transfers [tx]="tx" [showConfirmations]="true"></app-bisq-transfers>
<br>
</ng-template>
</ng-template>
<ng-template [ngIf]="isLoading && !error">
<div class="box">
<div class="row">
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.hash">Hash</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>
</div>
<div class="col">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="block.previous_hash|Transaction Previous Hash">Previous hash</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</table>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div class="clearfix"></div>
<div class="text-center">
Error loading block
<br>
<i>{{ error.status }}: {{ error.statusText }}</i>
</div>
</ng-template>
</div>

View File

@@ -1,44 +0,0 @@
.td-width {
width: 140px;
@media (min-width: 768px) {
width: 175px;
}
}
h1 {
margin: 0px;
padding: 0px;
@media (min-width: 576px) {
float: left;
margin-right: 10px;
}
}
.row{
flex-direction: column;
@media (min-width: 768px) {
flex-direction: row;
}
}
.block-container {
.table {
tr td {
&:last-child {
text-align: right;
@media (min-width: 992px) {
text-align: left;
}
}
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 992px) {
display: inline-block;
font-size: 14px;
margin-left: 10px;
}
}
}

View File

@@ -1,106 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { BisqBlock } from '../../bisq/bisq.interfaces';
import { Location } from '@angular/common';
import { BisqApiService } from '../bisq-api.service';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { Subscription, of } from 'rxjs';
import { switchMap, catchError } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { HttpErrorResponse } from '@angular/common/http';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-block',
templateUrl: './bisq-block.component.html',
styleUrls: ['./bisq-block.component.scss']
})
export class BisqBlockComponent implements OnInit, OnDestroy {
block: BisqBlock;
subscription: Subscription;
blockHash = '';
blockHeight = 0;
isLoading = true;
error: HttpErrorResponse | null;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private route: ActivatedRoute,
private seoService: SeoService,
private electrsApiService: ElectrsApiService,
private router: Router,
private location: Location,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.subscription = this.route.paramMap
.pipe(
switchMap((params: ParamMap) => {
const blockHash = params.get('id') || '';
document.body.scrollTo(0, 0);
this.isLoading = true;
this.error = null;
if (history.state.data && history.state.data.blockHeight) {
this.blockHeight = history.state.data.blockHeight;
}
if (history.state.data && history.state.data.block) {
this.blockHeight = history.state.data.block.height;
return of(history.state.data.block);
}
let isBlockHeight = false;
if (/^[0-9]+$/.test(blockHash)) {
isBlockHeight = true;
} else {
this.blockHash = blockHash;
}
if (isBlockHeight) {
return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
.pipe(
switchMap((hash) => {
if (!hash) {
return;
}
this.blockHash = hash;
this.location.replaceState(
this.router.createUrlTree(['/bisq/block/', hash]).toString()
);
this.seoService.updateCanonical(this.location.path());
return this.bisqApiService.getBlock$(this.blockHash)
.pipe(catchError(this.caughtHttpError.bind(this)));
}),
catchError(this.caughtHttpError.bind(this))
);
}
return this.bisqApiService.getBlock$(this.blockHash)
.pipe(catchError(this.caughtHttpError.bind(this)));
})
)
.subscribe((block: BisqBlock) => {
if (!block) {
this.seoService.logSoft404();
return;
}
this.isLoading = false;
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@bisq-block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.hash}:BLOCK_HASH:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.block:See all BSQ transactions in Bitcoin block ${block.height}:BLOCK_HEIGHT: (block hash ${block.hash}:BLOCK_HASH:).`);
this.block = block;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
caughtHttpError(err: HttpErrorResponse){
this.error = err;
this.seoService.logSoft404();
return of(null);
}
}

View File

@@ -1,40 +0,0 @@
<div class="container-xl" (window:resize)="onResize($event)">
<h1 style="float: left;" i18n="Bisq blocks header">BSQ Blocks</h1>
<br>
<div class="clearfix"></div>
<ng-container *ngIf="{ value: (blocks$ | async) } as blocks">
<div class="table-responsive-sm">
<table class="table table-borderless table-striped">
<thead>
<th style="width: 25%;" i18n="Bisq block height header">Height</th>
<th style="width: 25%;" i18n="Bisq block confirmed time header">Confirmed</th>
<th style="width: 25%;" i18n="Bisq block total BSQ tokens sent header">Total sent</th>
<th class="d-none d-md-block" style="width: 25%;" i18n="Bisq block transactions title">Transactions</th>
</thead>
<tbody *ngIf="blocks.value; else loadingTmpl">
<tr *ngFor="let block of blocks.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/block/' | relativeUrl, block.hash]" [state]="{ data: { block: block } }">{{ block.height }}</a></td>
<td><app-time kind="since" [time]="block.time / 1000" [fastRender]="true"></app-time></td>
<td>{{ calculateTotalOutput(block) / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
<td class="d-none d-md-block">{{ block.txs.length }}</td>
</tr>
</tbody>
</table>
</div>
<br>
<ngb-pagination *ngIf="blocks.value" class="pagination-container" [size]="paginationSize" [collectionSize]="blocks.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
<div class="clearfix"></div>
<br>
</ng-container>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of loadingItems">
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@@ -1,6 +0,0 @@
.pagination-container {
float: none;
@media(min-width: 400px){
float: right;
}
}

View File

@@ -1,91 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { BisqApiService } from '../bisq-api.service';
import { switchMap, map, take, mergeMap, tap } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { BisqBlock, BisqOutput, BisqTransaction } from '../bisq.interfaces';
import { SeoService } from '../../services/seo.service';
import { ActivatedRoute, Router } from '@angular/router';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-blocks',
templateUrl: './bisq-blocks.component.html',
styleUrls: ['./bisq-blocks.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqBlocksComponent implements OnInit {
blocks$: Observable<[BisqBlock[], number]>;
page = 1;
itemsPerPage: number;
contentSpace = window.innerHeight - (165 + 75);
fiveItemsPxSize = 250;
loadingItems: number[];
isLoading = true;
// @ts-ignore
paginationSize: 'sm' | 'lg' = 'md';
paginationMaxSize = 5;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private route: ActivatedRoute,
private router: Router,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@8a7b4bd44c0ac71b2e72de0398b303257f7d2f54:Blocks`);
this.seoService.setDescription($localize`:@@meta.description.bisq.blocks:See a list of recent Bitcoin blocks with BSQ transactions, total BSQ sent per block, and more.`);
this.itemsPerPage = Math.max(Math.round(this.contentSpace / this.fiveItemsPxSize) * 5, 10);
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 670) {
this.paginationSize = 'sm';
this.paginationMaxSize = 3;
}
this.blocks$ = this.route.queryParams
.pipe(
take(1),
tap((qp) => {
if (qp.page) {
this.page = parseInt(qp.page, 10);
}
}),
mergeMap(() => this.route.queryParams),
map((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
return newPage;
} else {
this.page = 1;
}
return 1;
}),
switchMap((page) => this.bisqApiService.listBlocks$((page - 1) * this.itemsPerPage, this.itemsPerPage)),
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)]),
);
}
calculateTotalOutput(block: BisqBlock): number {
return block.txs.reduce((a: number, tx: BisqTransaction) =>
a + tx.outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0), 0
);
}
trackByFn(index: number) {
return index;
}
pageChange(page: number) {
this.router.navigate([], {
queryParams: { page: page },
queryParamsHandling: 'merge',
});
}
onResize(event: any) {
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
}
}

View File

@@ -1,63 +0,0 @@
<div class="container-xl">
<h1 i18n="Bisq markets title">Bisq Trading Volume</h1>
<div id="volumeHolder">
<ng-template #loadingVolumes>
<div class="text-center loadingVolumes">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-container *ngIf="volumes$ | async as volumes; else loadingVolumes">
<app-lightweight-charts-area [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
</ng-container>
</div>
<br><br>
<div class="container-info">
<h1>
<ng-template [ngIf]="stateService.env.BASE_MODULE === 'bisq'" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
</h1>
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th i18n>Price</th>
<th><ng-container i18n="Trading volume 7D">Volume (7d)</ng-container> <button [disabled]="(sort$ | async) === 'volumes'" class="btn btn-link btn-sm" (click)="sort('volumes')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
</thead>
<tbody *ngIf="tickers.value; else loadingTmpl">
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
<td>
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
<ng-template #fiat>
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
</ng-template>
</td>
<td>
<app-fiat [value]="ticker.volume?.volume"></app-fiat>
</td>
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
</tr>
</tbody>
</table>
</div>
<br><br>
<h2 i18n="Latest Trades header">Latest Trades</h2>
<app-bisq-trades [trades$]="trades$"></app-bisq-trades>
</ng-container>
</div>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of [1, 2, 3, 4]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@@ -1,22 +0,0 @@
#volumeHolder {
height: 500px;
background-color: #000;
overflow: hidden;
display: flex;
justify-content: center;
}
.table {
max-width: 100%;
overflow: scroll;
}
.loadingVolumes {
position: relative;
top: 45%;
z-index: 100;
}
.container-info{
overflow-x: scroll;
}

View File

@@ -1,132 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
import { map, share, switchMap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { Trade } from '../bisq.interfaces';
@Component({
selector: 'app-bisq-dashboard',
templateUrl: './bisq-dashboard.component.html',
styleUrls: ['./bisq-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqDashboardComponent implements OnInit {
tickers$: Observable<any>;
volumes$: Observable<any>;
trades$: Observable<Trade[]>;
sort$ = new BehaviorSubject<string>('trades');
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.setTitle($localize`:@@meta.title.bisq.markets:Markets`);
this.seoService.setDescription($localize`:@@meta.description.bisq.markets:Explore the full Bitcoin ecosystem with The Mempool Open Source Project&reg;. See Bisq market prices, trading activity, and more.`);
this.websocketService.want(['blocks']);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
.pipe(
map((volumes) => {
const data = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.volume,
};
});
const linesData = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.num_trades,
};
});
return {
data: data,
linesData: linesData,
};
})
);
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
this.tickers$ = combineLatest([
this.bisqApiService.getMarketsTicker$(),
getMarkets,
this.bisqApiService.getMarketVolumesByTime$('7d'),
])
.pipe(
map(([tickers, markets, volumes]) => {
const newTickers = [];
for (const t in tickers) {
if (this.stateService.env.BASE_MODULE !== 'bisq') {
const pair = t.split('_');
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
continue;
}
}
const mappedTicker: any = tickers[t];
mappedTicker.pair_url = t;
mappedTicker.pair = t.replace('_', '/').toUpperCase();
mappedTicker.market = markets[t];
mappedTicker.volume = volumes[t];
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
newTickers.push(mappedTicker);
}
return newTickers;
}),
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
map(([sort, tickers]) => {
if (sort === 'trades') {
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
} else if (sort === 'volumes') {
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
} else if (sort === 'name') {
tickers.sort((a, b) => a.name.localeCompare(b.name));
}
return tickers;
})
);
this.trades$ = combineLatest([
this.bisqApiService.getMarketTrades$('all'),
getMarkets,
])
.pipe(
map(([trades, markets]) => {
if (this.stateService.env.BASE_MODULE !== 'bisq') {
trades = trades.filter((trade) => {
const pair = trade.market.split('_');
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
});
}
return trades.map((trade => {
trade._market = markets[trade.market];
return trade;
}));
})
);
}
trackByFn(index: number) {
return index;
}
sort(by: string) {
this.sort$.next(by);
}
}

View File

@@ -1 +0,0 @@
<fa-icon [icon]="iconProp" [fixedWidth]="true" [ngStyle]="{ 'color': '#' + color }"></fa-icon>

View File

@@ -1,87 +0,0 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { IconPrefix, IconName } from '@fortawesome/fontawesome-common-types';
@Component({
selector: 'app-bisq-icon',
templateUrl: './bisq-icon.component.html',
styleUrls: ['./bisq-icon.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqIconComponent implements OnChanges {
@Input() txType: string;
iconProp: [IconPrefix, IconName] = ['fas', 'leaf'];
color: string;
constructor() { }
ngOnChanges() {
switch (this.txType) {
case 'UNVERIFIED':
this.iconProp[1] = 'question';
this.color = 'ffac00';
break;
case 'INVALID':
this.iconProp[1] = 'exclamation-triangle';
this.color = 'ff4500';
break;
case 'GENESIS':
this.iconProp[1] = 'rocket';
this.color = '25B135';
break;
case 'TRANSFER_BSQ':
this.iconProp[1] = 'retweet';
this.color = 'a3a3a3';
break;
case 'PAY_TRADE_FEE':
this.iconProp[1] = 'leaf';
this.color = '689f43';
break;
case 'PROPOSAL':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'COMPENSATION_REQUEST':
this.iconProp[1] = 'money-bill';
this.color = '689f43';
break;
case 'REIMBURSEMENT_REQUEST':
this.iconProp[1] = 'money-bill';
this.color = '04a908';
break;
case 'BLIND_VOTE':
this.iconProp[1] = 'eye-slash';
this.color = '07579a';
break;
case 'VOTE_REVEAL':
this.iconProp[1] = 'eye';
this.color = '4AC5FF';
break;
case 'LOCKUP':
this.iconProp[1] = 'lock';
this.color = '0056c4';
break;
case 'UNLOCK':
this.iconProp[1] = 'lock-open';
this.color = '1d965f';
break;
case 'ASSET_LISTING_FEE':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'PROOF_OF_BURN':
this.iconProp[1] = 'file-alt';
this.color = '6c8b3b';
break;
case 'IRREGULAR':
this.iconProp[1] = 'exclamation-circle';
this.color = 'ffd700';
break;
default:
this.iconProp[1] = 'question';
this.color = 'ffac00';
}
// @ts-ignore
this.iconProp = this.iconProp.slice();
}
}

View File

@@ -1,124 +0,0 @@
<div class="container-xl">
<br>
<div class="row row-cols-1 row-cols-md-2">
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="bisq-dashboard.price-index-title">Bisq Price Index</h5>
<div class="big-fiat">
<span *ngIf="usdPrice$ | async as usdPrice; else loading">
<span [appColoredPrice]="usdPrice">{{ usdPrice | currency:'USD':'symbol':'1.2-2' }}</span>
</span>
</div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="bisq-dashboard.market-price-title">Bisq Market Price</h5>
<div class="big-fiat">
<span class="green-color" *ngIf="bisqMarketPrice; else loading">
<span [appColoredPrice]="bisqMarketPrice">{{ bisqMarketPrice | currency:'USD':'symbol':'1.2-2' }}</span>
</span>
</div>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title">US Dollar - BTC/USD</h5>
<div class="chart-container">
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
<app-lightweight-charts [height]="300" [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="2"></app-lightweight-charts>
</ng-container>
</div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" i18n="Bisq markets title">Bisq Trading Volume</h5>
<div class="chart-container">
<ng-container *ngIf="volumes$ | async as volumes; else loadingSpinner">
<app-lightweight-charts-area [height]="300" [data]="volumes.data" [lineData]="volumes.linesData"></app-lightweight-charts-area>
</ng-container>
</div>
</div>
</div>
</div>
</div>
<div class="row row-cols-1 row-cols-md-2">
<ng-container *ngIf="{ value: (tickers$ | async) } as tickers">
<div class="col mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center">
<ng-template [ngIf]="stateService.env.BASE_MODULE === 'bisq'" [ngIfElse]="nonOfficialMarkets" i18n="Bisq All Markets">Markets</ng-template>
<ng-template #nonOfficialMarkets i18n="Bisq Bitcoin Markets">Bitcoin Markets</ng-template>
</h5>
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th><ng-container i18n>Currency</ng-container> <button [disabled]="(sort$ | async) === 'name'" class="btn btn-link btn-sm" (click)="sort('name')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
<th i18n>Price</th>
<th><ng-container i18n="Trades amount 7D">Trades (7d)</ng-container> <button [disabled]="(sort$ | async) === 'trades'" class="btn btn-link btn-sm" (click)="sort('trades')"><fa-icon [icon]="['fas', 'chevron-down']" [fixedWidth]="true"></fa-icon></button></th>
</thead>
<tbody *ngIf="tickers.value; else loadingTmpl">
<tr *ngFor="let ticker of tickers.value; trackBy: trackByFn;">
<td><a [routerLink]="['/market' | relativeUrl, ticker.pair_url]">{{ ticker.name }})</a></td>
<td>
<app-fiat *ngIf="ticker.market.rtype === 'crypto'; else fiat" [value]="ticker.last * 100000000"></app-fiat>
<ng-template #fiat>
<span class="green-color">{{ ticker.last | currency: ticker.market.rsymbol }}</span>
</ng-template>
</td>
<td>{{ ticker.volume?.num_trades ? ticker.volume?.num_trades : 0 }}</td>
</tr>
</tbody>
</table>
</div>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
<div class="col mb-4">
<div class="card">
<div class="card-body">
<h5 class="card-title text-center" i18n="Latest Trades header">Latest Trades</h5>
<app-bisq-trades [trades$]="trades$" view="small"></app-bisq-trades>
<div class="text-center"><a href="" [routerLink]="['/markets' | relativeUrl]" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div>
</ng-container>
</div>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of [1, 2, 3]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
<ng-template #loadingSpinner>
<div class="text-center loadingGraphs">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #loading>
<div class="skeleton-loader shorter"></div>
</ng-template>

View File

@@ -1,112 +0,0 @@
#volumeHolder {
height: 500px;
background-color: #000;
overflow: hidden;
display: flex;
justify-content: center;
}
.table {
max-width: 100%;
overflow: scroll;
}
.loadingGraphs {
position: relative;
top: 45%;
z-index: 100;
}
.table-container {
overflow: scroll;
scrollbar-width: none;
font-size: 13px;
&::-webkit-scrollbar {
display: none;
}
@media(min-width: 576px){
font-size: 16px;
}
thead th{
text-align: right;
&:first-child {
text-align: left;
}
&:nth-child(3) {
display: none;
@media(min-width: 1100px){
display: table-cell;
}
}
}
tr {
td {
text-align: right;
&:first-child {
text-align: left;
}
&:nth-child(3) {
display: none;
@media(min-width: 1100px){
display: table-cell;
}
}
}
}
}
.chart-container {
height: 300px;
}
.big-fiat {
color: #3bcc49;
font-size: 26px;
}
.card {
background-color: #1d1f31;
height: 100%;
}
.card-title {
color: #4a68b9;
font-size: 1rem;
}
.info-block {
float: left;
width: 350px;
line-height: 25px;
}
.progress {
display: inline-flex;
width: 100%;
background-color: #2d3348;
height: 1.1rem;
}
.bg-warning {
background-color: #b58800 !important;
}
.skeleton-loader {
max-width: 100%;
&.shorter {
max-width: 150px;
}
}
.more-padding {
padding: 1.25rem 2rem 1.25rem 2rem;
}
.graph-card {
height: 100%;
@media (min-width: 992px) {
height: 385px;
}
}

View File

@@ -1,193 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable, combineLatest, BehaviorSubject, of } from 'rxjs';
import { map, share, switchMap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { Trade } from '../bisq.interfaces';
@Component({
selector: 'app-main-bisq-dashboard',
templateUrl: './bisq-main-dashboard.component.html',
styleUrls: ['./bisq-main-dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqMainDashboardComponent implements OnInit {
tickers$: Observable<any>;
volumes$: Observable<any>;
trades$: Observable<Trade[]>;
sort$ = new BehaviorSubject<string>('trades');
hlocData$: Observable<any>;
usdPrice$: Observable<number>;
isLoadingGraph = true;
bisqMarketPrice = 0;
allowCryptoCoins = ['usdc', 'l-btc', 'bsq'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
public stateService: StateService,
private seoService: SeoService,
) { }
ngOnInit(): void {
this.seoService.resetTitle();
this.seoService.resetDescription();
this.websocketService.want(['blocks']);
this.usdPrice$ = this.stateService.conversions$.asObservable().pipe(
map((conversions) => conversions.USD)
);
this.volumes$ = this.bisqApiService.getAllVolumesDay$()
.pipe(
map((volumes) => {
const data = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.volume,
};
});
const linesData = volumes.map((volume) => {
return {
time: volume.period_start,
value: volume.num_trades,
};
});
return {
data: data,
linesData: linesData,
};
})
);
const getMarkets = this.bisqApiService.getMarkets$().pipe(share());
this.tickers$ = combineLatest([
this.bisqApiService.getMarketsTicker$(),
getMarkets,
this.bisqApiService.getMarketVolumesByTime$('7d'),
])
.pipe(
map(([tickers, markets, volumes]) => {
const newTickers = [];
for (const t in tickers) {
if (this.stateService.env.BASE_MODULE !== 'bisq') {
const pair = t.split('_');
if (pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1) {
continue;
}
}
const mappedTicker: any = tickers[t];
mappedTicker.pair_url = t;
mappedTicker.pair = t.replace('_', '/').toUpperCase();
mappedTicker.market = markets[t];
mappedTicker.volume = volumes[t];
mappedTicker.name = `${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lname : mappedTicker.market.rname} (${mappedTicker.market.rtype === 'crypto' ? mappedTicker.market.lsymbol : mappedTicker.market.rsymbol}`;
newTickers.push(mappedTicker);
}
return newTickers;
}),
switchMap((tickers) => combineLatest([this.sort$, of(tickers)])),
map(([sort, tickers]) => {
if (sort === 'trades') {
tickers.sort((a, b) => (b.volume && b.volume.num_trades || 0) - (a.volume && a.volume.num_trades || 0));
} else if (sort === 'volumes') {
tickers.sort((a, b) => (b.volume && b.volume.volume || 0) - (a.volume && a.volume.volume || 0));
} else if (sort === 'name') {
tickers.sort((a, b) => a.name.localeCompare(b.name));
}
return tickers.slice(0, 10);
})
);
this.trades$ = combineLatest([
this.bisqApiService.getMarketTrades$('all'),
getMarkets,
])
.pipe(
map(([trades, markets]) => {
if (this.stateService.env.BASE_MODULE !== 'bisq') {
trades = trades.filter((trade) => {
const pair = trade.market.split('_');
return !(pair[1] === 'btc' && this.allowCryptoCoins.indexOf(pair[0]) === -1);
});
}
return trades.map((trade => {
trade._market = markets[trade.market];
return trade;
})).slice(0, 10);
})
);
this.hlocData$ = this.bisqApiService.getMarketsHloc$('btc_usd', 'day')
.pipe(
map((hlocData) => {
this.isLoadingGraph = false;
hlocData = hlocData.map((h) => {
h.time = h.period_start;
return h;
});
const hlocVolume = hlocData.map((h) => {
return {
time: h.time,
value: h.volume_right,
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
};
});
// Add whitespace
if (hlocData.length > 1) {
const newHloc = [];
newHloc.push(hlocData[0]);
const period = 86400;
let periods = 0;
const startingDate = hlocData[0].period_start;
let index = 1;
while (true) {
periods++;
if (hlocData[index].period_start > startingDate + period * periods) {
newHloc.push({
time: startingDate + period * periods,
});
} else {
newHloc.push(hlocData[index]);
index++;
if (!hlocData[index]) {
break;
}
}
}
hlocData = newHloc;
}
this.bisqMarketPrice = hlocData[hlocData.length - 1].close;
return {
hloc: hlocData,
volume: hlocVolume,
};
}),
);
}
trackByFn(index: number) {
return index;
}
sort(by: string) {
this.sort$.next(by);
}
}

View File

@@ -1,112 +0,0 @@
<div class="container-xl">
<ng-container *ngIf="hlocData$ | async as hlocData; else loadingSpinner">
<ng-container *ngIf="currency$ | async as currency; else loadingSpinner">
<h1>{{ currency.market.rtype === 'crypto' ? currency.market.lname : currency.market.rname }} - {{ currency.pair }}</h1>
<div class="priceheader">
<ng-container *ngIf="currency.market.rtype === 'fiat'; else headerPriceCrypto"><span class="green-color">{{ hlocData.hloc[hlocData.hloc.length - 1].close | currency: currency.market.rsymbol }}</span></ng-container>
<ng-template #headerPriceCrypto>{{ hlocData.hloc[hlocData.hloc.length - 1].close | number: '1.' + currency.market.rprecision + '-' + currency.market.rprecision }} {{ currency.market.rsymbol }}</ng-template>
</div>
<form [formGroup]="radioGroupForm" class="mb-3 radio-form">
<div class="btn-group btn-group-toggle" name="radioBasic">
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_hour'">
<input type="radio" [value]="'half_hour'" (click)="setFragment('half_hour')" formControlName="interval"> 30M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'hour'">
<input type="radio" [value]="'hour'" (click)="setFragment('hour')" formControlName="interval"> 1H
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'half_day'">
<input type="radio" [value]="'half_day'" (click)="setFragment('half_day')" formControlName="interval"> 12H
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'day'">
<input type="radio" [value]="'day'" (click)="setFragment('day')" formControlName="interval"> 1D
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'week'">
<input type="radio" [value]="'week'" (click)="setFragment('week')" formControlName="interval"> 1W
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'month'">
<input type="radio" [value]="'month'" (click)="setFragment('month')" formControlName="interval"> 1M
</label>
<label class="btn btn-primary btn-sm" [class.active]="radioGroupForm.get('interval').value === 'year'">
<input type="radio" [value]="'year'" (click)="setFragment('year')" formControlName="interval"> 1Y
</label>
</div>
</form>
<div class="clearfix"></div>
<div id="graphHolder">
<div class="text-center loadingChart" [hidden]="!isLoadingGraph">
<div class="spinner-border text-light"></div>
</div>
<app-lightweight-charts [data]="hlocData.hloc" [volumeData]="hlocData.volume" [precision]="currency.market.rtype === 'crypto' ? currency.market.lprecision : currency.market.rprecision"></app-lightweight-charts>
</div>
<br>
<ng-container *ngIf="offers$ | async as offers; else loadingSpinner">
<div class="row row-cols-1 row-cols-md-2">
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.buys, direction: 'BUY', market: currency.market }"></ng-container>
<ng-container *ngTemplateOutlet="offersList; context: { offers: offers.sells, direction: 'SELL', market: currency.market }"></ng-container>
</div>
</ng-container>
<br><br>
<ng-container *ngIf="trades$ | async as trades; else loadingSpinner">
<h2 i18n="Latest Trades header">Latest Trades</h2>
<app-bisq-trades [trades$]="trades$" [market]="currency.market"></app-bisq-trades>
</ng-container>
</ng-container>
</ng-container>
</div>
<ng-template #offersList let-offers="offers" let-direction="direction", let-market="market">
<div class="col">
<h2>
<ng-template [ngIf]="direction === 'BUY'" [ngIfElse]="sellOffers" i18n="Bisq Buy Offers">Buy Offers</ng-template>
<ng-template #sellOffers i18n="Bisq Sell Offers">Sell Offers</ng-template>
</h2>
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th i18n>Price</th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol }"></ng-container></th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.rsymbol }"></ng-container></th>
</thead>
<tbody>
<tr *ngFor="let offer of offers">
<td>
<ng-container *ngIf="market.rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ offer.price | currency: market.rsymbol }}</span></ng-container>
<ng-template #priceCrypto>{{ offer.price | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
</td>
<td>
<ng-container *ngIf="market.ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ offer.amount | currency: market.rsymbol }}</span></ng-container>
<ng-template #amountCrypto>{{ offer.amount | number: '1.2-' + market.lprecision }} <span class="symbol">{{ market.lsymbol }}</span></ng-template>
</td>
<td>
<ng-container *ngIf="market.rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ offer.volume | currency: market.rsymbol }}</span></ng-container>
<ng-template #volumeCrypto>{{ offer.volume | number: '1.2-' + market.rprecision }} <span class="symbol">{{ market.rsymbol }}</span></ng-template>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</ng-template>
<ng-template #loadingSpinner>
<br>
<br>
<div class="text-center">
<div class="spinner-border text-light"></div>
</div>
</ng-template>
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>

View File

@@ -1,46 +0,0 @@
.priceheader {
font-size: 24px;
@media(min-width: 576px){
float: left;
}
}
.radio-form {
@media(min-width: 576px){
float: right;
}
}
.loadingChart {
z-index: 100;
position: absolute;
top: 50%;
left: 50%;
}
#graphHolder {
height: 550px;
overflow: hidden;
}
.col {
&:last-child{
margin-top: 50px;
@media(min-width: 576px){
margin-top: 0px;
}
}
}
.table-container {
overflow: scroll;
-ms-overflow-style: none;
scrollbar-width: none;
font-size: 13px;
@media(min-width: 576px){
font-size: 16px;
}
&::-webkit-scrollbar {
display: none;
}
}

View File

@@ -1,159 +0,0 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { combineLatest, merge, Observable, of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { SeoService } from '../../services/seo.service';
import { WebsocketService } from '../../services/websocket.service';
import { BisqApiService } from '../bisq-api.service';
import { OffersMarket, Trade } from '../bisq.interfaces';
@Component({
selector: 'app-bisq-market',
templateUrl: './bisq-market.component.html',
styleUrls: ['./bisq-market.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqMarketComponent implements OnInit, OnDestroy {
hlocData$: Observable<any>;
currency$: Observable<any>;
offers$: Observable<OffersMarket>;
trades$: Observable<Trade[]>;
radioGroupForm: UntypedFormGroup;
defaultInterval = 'day';
isLoadingGraph = false;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private bisqApiService: BisqApiService,
private formBuilder: UntypedFormBuilder,
private seoService: SeoService,
private router: Router,
) { }
ngOnInit(): void {
this.radioGroupForm = this.formBuilder.group({
interval: [this.defaultInterval],
});
if (['half_hour', 'hour', 'half_day', 'day', 'week', 'month', 'year', 'auto'].indexOf(this.route.snapshot.fragment) > -1) {
this.radioGroupForm.controls.interval.setValue(this.route.snapshot.fragment, { emitEvent: false });
}
this.currency$ = this.bisqApiService.getMarkets$()
.pipe(
switchMap((markets) => combineLatest([of(markets), this.route.paramMap])),
map(([markets, routeParams]) => {
const pair = routeParams.get('pair');
const pairUpperCase = pair.replace('_', '/').toUpperCase();
this.seoService.setTitle($localize`:@@meta.title.bisq.market:Bisq market: ${pairUpperCase}`);
this.seoService.setDescription($localize`:@@meta.description.bisq.market:See price history, current buy/sell offers, and latest trades for the ${pairUpperCase} market on Bisq.`);
return {
pair: pairUpperCase,
market: markets[pair],
};
})
);
this.trades$ = this.route.paramMap
.pipe(
map(routeParams => routeParams.get('pair')),
switchMap((marketPair) => this.bisqApiService.getMarketTrades$(marketPair)),
);
this.offers$ = this.route.paramMap
.pipe(
map(routeParams => routeParams.get('pair')),
switchMap((marketPair) => this.bisqApiService.getMarketOffers$(marketPair)),
map((offers) => offers[Object.keys(offers)[0]])
);
this.hlocData$ = combineLatest([
this.route.paramMap,
merge(this.radioGroupForm.get('interval').valueChanges, of(this.radioGroupForm.get('interval').value)),
])
.pipe(
switchMap(([routeParams, interval]) => {
this.isLoadingGraph = true;
const pair = routeParams.get('pair');
return this.bisqApiService.getMarketsHloc$(pair, interval);
}),
map((hlocData) => {
this.isLoadingGraph = false;
hlocData = hlocData.map((h) => {
h.time = h.period_start;
return h;
});
const hlocVolume = hlocData.map((h) => {
return {
time: h.time,
value: h.volume_right,
color: h.close > h.avg ? 'rgba(0, 41, 74, 0.7)' : 'rgba(0, 41, 74, 1)',
};
});
// Add whitespace
if (hlocData.length > 1) {
const newHloc = [];
newHloc.push(hlocData[0]);
const period = this.getUnixTimestampFromInterval(this.radioGroupForm.get('interval').value); // temp
let periods = 0;
const startingDate = hlocData[0].period_start;
let index = 1;
while (true) {
periods++;
if (hlocData[index].period_start > startingDate + period * periods) {
newHloc.push({
time: startingDate + period * periods,
});
} else {
newHloc.push(hlocData[index]);
index++;
if (!hlocData[index]) {
break;
}
}
}
hlocData = newHloc;
}
return {
hloc: hlocData,
volume: hlocVolume,
};
}),
);
}
setFragment(fragment: string) {
this.router.navigate([], {
relativeTo: this.route,
queryParamsHandling: 'merge',
fragment: fragment
});
}
ngOnDestroy(): void {
this.websocketService.stopTrackingBisqMarket();
}
getUnixTimestampFromInterval(interval: string): number {
switch (interval) {
case 'minute': return 60;
case 'half_hour': return 1800;
case 'hour': return 3600;
case 'half_day': return 43200;
case 'day': return 86400;
case 'week': return 604800;
case 'month': return 2592000;
case 'year': return 31579200;
}
}
}

View File

@@ -1,86 +0,0 @@
<div class="container-xl">
<h1 style="float: left;" i18n="BSQ statistics header">BSQ statistics</h1>
<br>
<div class="clearfix"></div>
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody *ngIf="!isLoading; else loadingTemplate">
<tr>
<td class="td-width" i18n="BSQ existing amount">Existing amount</td>
<td>{{ (stats.minted - stats.burnt) | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ minted amount">Minted amount</td>
<td>{{ stats.minted | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ burnt amount">Burnt amount</td>
<td>{{ stats.burnt | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="BSQ addresses">Addresses</td>
<td>{{ stats.addresses | number }}</td>
</tr>
<tr>
<td i18n="BSQ unspent transaction outputs">Unspent TXOs</td>
<td>{{ stats.unspent_txos | number }}</td>
</tr>
<tr>
<td i18n="BSQ spent transaction outputs">Spent TXOs</td>
<td>{{ stats.spent_txos | number }}</td>
</tr>
<tr>
<td i18n>Price</td>
<td><app-fiat [value]="price"></app-fiat></td>
</tr>
<tr>
<td i18n="BSQ token market cap">Market cap</td>
<td><app-fiat [value]="price * (stats.minted - stats.burnt)"></app-fiat></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md"></div>
</div>
</div>
<ng-template #loadingTemplate>
<tbody>
<tr>
<td class="td-width" i18n>Existing amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Minted amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Burnt amount</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Addresses</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Unspent TXOs</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td>Spent TXOs</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Price</td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td i18n>Market cap</td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</ng-template>

View File

@@ -1,18 +0,0 @@
.td-width {
width: 250px;
}
@media (max-width: 767.98px) {
.td-width {
width: 175px;
}
}
.fiat {
display: block;
font-size: 13px;
@media (min-width: 768px) {
font-size: 14px;
display: inline-block;
}
}

View File

@@ -1,42 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { BisqApiService } from '../bisq-api.service';
import { BisqStats } from '../bisq.interfaces';
import { SeoService } from '../../services/seo.service';
import { StateService } from '../../services/state.service';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-stats',
templateUrl: './bisq-stats.component.html',
styleUrls: ['./bisq-stats.component.scss']
})
export class BisqStatsComponent implements OnInit {
isLoading = true;
stats: BisqStats;
price: number;
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private stateService: StateService,
) { }
ngOnInit() {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@2a30a4cdb123a03facc5ab8c5b3e6d8b8dbbc3d4:BSQ statistics`);
this.seoService.setDescription($localize`:@@meta.description.bisq.stats:See high-level stats on the BSQ economy: supply metrics, number of addresses, BSQ price, market cap, and more.`);
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {
this.price = bsqPrice;
});
this.bisqApiService.getStats$()
.subscribe((stats) => {
this.isLoading = false;
this.stats = stats;
});
}
}

View File

@@ -1,46 +0,0 @@
<div class="table-container">
<table class="table table-borderless table-striped">
<thead>
<th i18n>Date</th>
<th *ngIf="view === 'all'" i18n>Price</th>
<th><ng-container *ngTemplateOutlet="amount; context: {$implicit: 'BTC' }"></ng-container></th>
<th>
<ng-template [ngIf]="market" [ngIfElse]="noMarket"><ng-container *ngTemplateOutlet="amount; context: {$implicit: market.lsymbol === 'BTC' ? market.rsymbol : market.lsymbol }"></ng-container></ng-template>
<ng-template #noMarket i18n>Amount</ng-template>
</th>
</thead>
<tbody *ngIf="(trades$ | async) as trades; else loadingTmpl">
<tr *ngFor="let trade of trades;">
<td>
&lrm;{{ trade.trade_date | date:'yyyy-MM-dd HH:mm' }}
</td>
<td *ngIf="view === 'all'">
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else priceCrypto"><span class="green-color">{{ trade.price | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #priceCrypto>{{ trade.price | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
</td>
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeVolume : tradeAmount"></ng-container>
<ng-container *ngTemplateOutlet="(trade._market || market).rsymbol === 'BTC' ? tradeAmount : tradeVolume"></ng-container>
<ng-template #tradeAmount>
<td>
<ng-container *ngIf="(trade._market || market).ltype === 'fiat'; else amountCrypto"><span class="green-color">{{ trade.amount | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #amountCrypto>{{ trade.amount | number: '1.2-' + (trade._market || market).lprecision }} <span class="symbol">{{ (trade._market || market).lsymbol }}</span></ng-template>
</td>
</ng-template>
<ng-template #tradeVolume>
<td>
<ng-container *ngIf="(trade._market || market).rtype === 'fiat'; else volumeCrypto"><span class="green-color">{{ trade.volume | currency: (trade._market || market).rsymbol }}</span></ng-container>
<ng-template #volumeCrypto>{{ trade.volume | number: '1.2-' + (trade._market || market).rprecision }} <span class="symbol">{{ (trade._market || market).rsymbol }}</span></ng-template>
</td>
</ng-template>
</tr>
</tbody>
</table>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of [1,2,3,4,5,6,7,8,9,10]">
<td *ngFor="let j of loadingColumns"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>
<ng-template #amount let-i i18n="Trade amount (Symbol)">Amount ({{ i }})</ng-template>

View File

@@ -1,38 +0,0 @@
.table-container {
overflow: scroll;
scrollbar-width: none;
font-size: 13px;
&::-webkit-scrollbar {
display: none;
}
@media(min-width: 576px){
font-size: 16px;
}
thead th{
text-align: right;
&:first-child{
text-align: left;
}
&:nth-child(2) {
display: none;
@media(min-width: 1100px){
display: table-cell;
}
}
}
tr {
td {
text-align: right;
&:first-child{
text-align: left;
}
&:nth-child(2) {
display: none;
@media(min-width: 1100px){
display: table-cell;
}
}
}
}
}

View File

@@ -1,22 +0,0 @@
import { ChangeDetectionStrategy, Component, Input, OnChanges, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
@Component({
selector: 'app-bisq-trades',
templateUrl: './bisq-trades.component.html',
styleUrls: ['./bisq-trades.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqTradesComponent implements OnChanges {
@Input() trades$: Observable<any>;
@Input() market: any;
@Input() view: 'all' | 'small' = 'all';
loadingColumns = [1, 2, 3, 4];
ngOnChanges() {
if (this.view === 'small') {
this.loadingColumns = [1, 2, 3];
}
}
}

View File

@@ -1,36 +0,0 @@
<div class="box">
<div class="row">
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="transaction.inputs">Inputs</td>
<td>{{ totalInput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="transaction.outputs">Outputs</td>
<td>{{ totalOutput / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
<tr>
<td i18n="asset.issued-amount|Liquid Asset issued amount">Issued amount</td>
<td>{{ totalIssued / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-md">
<table class="table table-borderless table-striped">
<tbody class="mobile-even">
<tr>
<td class="td-width" i18n>Type</td>
<td><app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon> {{ tx.txTypeDisplayString }}</td>
</tr>
<tr>
<td i18n="transaction.version">Version</td>
<td>{{ tx.txVersion }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>

View File

@@ -1,22 +0,0 @@
@media (max-width: 767.98px) {
.td-width {
width: 150px;
}
.mobile-even tr:nth-of-type(even) {
background-color: #181b2d;
}
.mobile-even tr:nth-of-type(odd) {
background-color: inherit;
}
}
.table {
tr td {
&:last-child{
text-align: right;
@media(min-width: 768px){
text-align: left;
}
}
}
}

View File

@@ -1,26 +0,0 @@
import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { BisqTransaction } from '../../bisq/bisq.interfaces';
@Component({
selector: 'app-bisq-transaction-details',
templateUrl: './bisq-transaction-details.component.html',
styleUrls: ['./bisq-transaction-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BisqTransactionDetailsComponent implements OnChanges {
@Input() tx: BisqTransaction;
totalInput: number;
totalOutput: number;
totalIssued: number;
constructor() { }
ngOnChanges() {
this.totalInput = this.tx.inputs.filter((input) => input.isVerified).reduce((acc, input) => acc + input.bsqAmount, 0);
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
this.totalIssued = this.tx.outputs
.filter((output) => output.isVerified && output.txOutputType === 'ISSUANCE_CANDIDATE_OUTPUT')
.reduce((acc, output) => acc + output.bsqAmount, 0);
}
}

View File

@@ -1,216 +0,0 @@
<div class="container-xl">
<ng-template [ngIf]="!isLoading && !error">
<div class="title-block">
<div class="title">
<h1 i18n="shared.transaction">Transaction</h1>
</div>
<span class="tx-link">
<span class="txid">
<app-truncate [text]="bisqTx.id" [lastChars]="12" [link]="['/tx/' | relativeUrl, bisqTx.id]">
<app-clipboard [text]="bisqTx.id"></app-clipboard>
</app-truncate>
</span>
</span>
<span class="grow"></span>
<div class="container-buttons">
<div *ngIf="(latestBlock$ | async) as latestBlock">
<app-confirmations [chainTip]="latestBlock?.height" [height]="bisqTx.blockHeight" [hideUnconfirmed]="true" buttonClass="float-right"></app-confirmations>
</div>
</div>
</div>
<div class="clearfix"></div>
<div class="box transaction-container">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td i18n="block.timestamp">Timestamp</td>
<td>
&lrm;{{ bisqTx.time | date:'yyyy-MM-dd HH:mm' }}
<div class="lg-inline">
<i class="symbol">(<app-time kind="since" [time]="bisqTx.time / 1000" [fastRender]="true"></app-time>)</i>
</div>
</td>
</tr>
<tr>
<td class="td-width" i18n="transaction.included-in-block|Transaction included in block">Included in block</td>
<td>
<a [routerLink]="['/block/' | relativeUrl, bisqTx.blockHash]" [state]="{ data: { blockHeight: bisqTx.blockHeight } }">{{ bisqTx.blockHeight }}</a>
</td>
</tr>
<tr>
<td class="td-width" i18n="transaction.features|Transaction features">Features</td>
<td>
<app-tx-features *ngIf="tx; else loadingTx" [tx]="tx"></app-tx-features>
<ng-template #loadingTx>
<span class="skeleton-loader"></span>
</ng-template>
</td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width" i18n="BSQ burnt amount">Burnt amount</td>
<td>
{{ bisqTx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="fiat"><app-bsq-amount [bsq]="bisqTx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span>
</tr>
<tr>
<td *only-vsize i18n="transaction.fee-per-vbyte|Transaction fee">Fee per vByte</td>
<td *only-weight i18n="transaction.fee-per-wu|Transaction fee">Fee per weight unit</td>
<td *ngIf="!isLoadingTx; else loadingTxFee">
<app-fee-rate [fee]="tx.fee" [weight]="tx.weight"></app-fee-rate>
&nbsp;
<app-tx-fee-rating [tx]="tx"></app-tx-fee-rating>
</td>
<ng-template #loadingTxFee>
<td><span class="skeleton-loader"></span></td>
</ng-template>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<div class="title">
<h2 i18n="transaction.details">Details</h2>
</div>
<app-bisq-transaction-details [tx]="bisqTx"></app-bisq-transaction-details>
<br>
<div class="title">
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
</div>
<app-bisq-transfers [tx]="bisqTx"></app-bisq-transfers>
<br>
</ng-template>
<ng-template [ngIf]="isLoading && !error">
<div class="clearfix"></div>
<div class="title-block">
<div class="title">
<h1 i18n="shared.transaction">Transaction</h1>
</div>
</div>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td class="td-width"><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<br>
<div class="title">
<h2 i18n="transaction.details">Details</h2>
</div>
<div class="box">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
<td><span class="skeleton-loader"></span></td>
</tr>
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<br>
<div class="title">
<h2 i18n="transaction.inputs-and-outputs|Transaction inputs and outputs">Inputs & Outputs</h2>
</div>
<div class="box">
<div class="row">
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
<div class="col-sm">
<table class="table table-borderless table-striped">
<tbody>
<tr>
<td><span class="skeleton-loader"></span></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</ng-template>
<ng-template [ngIf]="error">
<div class="clearfix"></div>
<div class="text-center">
Error loading Bisq transaction
<br><br>
<i>{{ error.status }}: {{ error.statusText }}</i>
</div>
</ng-template>
</div>

View File

@@ -1,130 +0,0 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import { BisqTransaction } from '../../bisq/bisq.interfaces';
import { switchMap, map, catchError } from 'rxjs/operators';
import { of, Observable, Subscription } from 'rxjs';
import { StateService } from '../../services/state.service';
import { Block, Transaction } from '../../interfaces/electrs.interface';
import { BisqApiService } from '../bisq-api.service';
import { SeoService } from '../../services/seo.service';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { HttpErrorResponse } from '@angular/common/http';
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-transaction',
templateUrl: './bisq-transaction.component.html',
styleUrls: ['./../../components/transaction/transaction.component.scss']
})
export class BisqTransactionComponent implements OnInit, OnDestroy {
bisqTx: BisqTransaction;
tx: Transaction;
latestBlock$: Observable<Block>;
txId: string;
price: number;
isLoading = true;
isLoadingTx = true;
error = null;
subscription: Subscription;
constructor(
private websocketService: WebsocketService,
private route: ActivatedRoute,
private bisqApiService: BisqApiService,
private electrsApiService: ElectrsApiService,
private stateService: StateService,
private seoService: SeoService,
private router: Router,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.subscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.isLoading = true;
this.isLoadingTx = true;
this.error = null;
document.body.scrollTo(0, 0);
this.txId = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transaction:See inputs, outputs, transaction type, burnt amount, and more for transaction with txid ${this.txId}:INTERPOLATION:.`);
if (history.state.data) {
return of(history.state.data);
}
return this.bisqApiService.getTransaction$(this.txId)
.pipe(
catchError((bisqTxError: HttpErrorResponse) => {
if (bisqTxError.status === 404) {
return this.electrsApiService.getTransaction$(this.txId)
.pipe(
map((tx) => {
if (tx.status.confirmed) {
this.error = {
status: 200,
statusText: 'Transaction is confirmed but not available in the Bisq database, please try reloading this page.'
};
return null;
}
return tx;
}),
catchError((txError: HttpErrorResponse) => {
console.log(txError);
this.error = txError;
this.seoService.logSoft404();
return of(null);
})
);
}
this.error = bisqTxError;
this.seoService.logSoft404();
return of(null);
})
);
}),
switchMap((tx) => {
if (!tx) {
return of(null);
}
if (tx.version) {
if (this.stateService.env.BASE_MODULE === 'bisq') {
window.location.replace('https://mempool.space/tx/' + this.txId);
} else {
this.router.navigate(['/tx/', this.txId], { state: { data: tx, bsqTx: true }});
}
return of(null);
}
this.bisqTx = tx;
this.isLoading = false;
return this.electrsApiService.getTransaction$(this.txId);
}),
)
.subscribe((tx) => {
this.isLoadingTx = false;
if (!tx) {
this.seoService.logSoft404();
return;
}
this.tx = tx;
},
(error) => {
this.error = error;
});
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
this.stateService.bsqPrice$
.subscribe((bsqPrice) => {
this.price = bsqPrice;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}

View File

@@ -1,56 +0,0 @@
<div class="container-xl" (window:resize)="onResize($event)">
<h1 style="float: left;" i18n>BSQ Transactions</h1>
<div class="d-block float-right" id="filter">
<form [formGroup]="radioGroupForm">
<ngx-bootstrap-multiselect [options]="txTypeOptions" [settings]="txTypeDropdownSettings" [texts]="txTypeDropdownTexts" formControlName="txTypes"></ngx-bootstrap-multiselect>
</form>
</div>
<br>
<div class="clearfix"></div>
<ng-container *ngIf="{ value: (transactions$ | async) } as transactions">
<table class="table table-borderless table-striped">
<thead>
<th style="width: 20%;" i18n>TXID</th>
<th class="d-none d-md-block" style="width: 100%;" i18n>Type</th>
<th style="width: 20%;" i18n>Amount</th>
<th style="width: 20%;" i18n="transaction.confirmed|Transaction Confirmed state">Confirmed</th>
<th class="d-none d-md-block" i18n>Height</th>
</thead>
<tbody *ngIf="transactions.value; else loadingTmpl">
<tr *ngFor="let tx of transactions.value[0]; trackBy: trackByFn">
<td><a [routerLink]="['/tx/' | relativeUrl, tx.id]" [state]="{ data: tx }">{{ tx.id | slice : 0 : 8 }}</a></td>
<td class="d-none d-md-block">
<app-bisq-icon class="mr-1" [txType]="tx.txType"></app-bisq-icon>
<span class="d-none d-md-inline"> {{ getStringByTxType(tx.txType) }}</span>
</td>
<td>
<app-bisq-icon class="d-inline d-md-none mr-1" [txType]="tx.txType"></app-bisq-icon>
<ng-template [ngIf]="tx.txType === 'PAY_TRADE_FEE' || tx.txType === 'ASSET_LISTING_FEE'" [ngIfElse]="defaultTxType">
{{ tx.burntFee / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template>
<ng-template #defaultTxType>
{{ calculateTotalOutput(tx.outputs) / 100 | number: '1.2-2' }} <span class="d-none d-md-inline symbol">BSQ</span>
</ng-template>
</td>
<td><app-time kind="since" [time]="tx.time / 1000" [fastRender]="true"></app-time></td>
<td class="d-none d-md-block"><a [routerLink]="['/block/' | relativeUrl, tx.blockHash]" [state]="{ data: { blockHeight: tx.blockHeight } }">{{ tx.blockHeight }}</a></td>
</tr>
</tbody>
</table>
<br>
<ngb-pagination class="pagination-container" *ngIf="transactions.value" [collectionSize]="transactions.value[1]" [rotate]="true" [pageSize]="itemsPerPage" [(page)]="page" (pageChange)="pageChange(page)" [maxSize]="paginationMaxSize" [boundaryLinks]="true" [ellipses]="false"></ngb-pagination>
</ng-container>
</div>
<ng-template #loadingTmpl>
<tr *ngFor="let i of loadingItems">
<td *ngFor="let j of [1, 2, 3, 4, 5]"><span class="skeleton-loader"></span></td>
</tr>
</ng-template>

View File

@@ -1,23 +0,0 @@
label {
padding: 0.25rem 1rem;
white-space: nowrap;
}
:host ::ng-deep .dropdown-menu {
right: 0px;
left: inherit;
}
.pagination-container {
float: none;
@media(min-width: 400px){
float: right;
}
}
.container-xl {
padding-bottom: 60px;
@media(min-width: 400px){
padding-bottom: 100px;
}
}

View File

@@ -1,166 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { BisqTransaction, BisqOutput } from '../bisq.interfaces';
import { Observable, Subscription } from 'rxjs';
import { switchMap, map, tap } from 'rxjs/operators';
import { BisqApiService } from '../bisq-api.service';
import { SeoService } from '../../services/seo.service';
import { UntypedFormGroup, UntypedFormBuilder } from '@angular/forms';
import { Router, ActivatedRoute } from '@angular/router';
import { IMultiSelectOption, IMultiSelectSettings, IMultiSelectTexts } from '../../components/ngx-bootstrap-multiselect/types'
import { WebsocketService } from '../../services/websocket.service';
@Component({
selector: 'app-bisq-transactions',
templateUrl: './bisq-transactions.component.html',
styleUrls: ['./bisq-transactions.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqTransactionsComponent implements OnInit, OnDestroy {
transactions$: Observable<[BisqTransaction[], number]>;
page = 1;
itemsPerPage = 50;
fiveItemsPxSize = 250;
isLoading = true;
loadingItems: number[];
radioGroupForm: UntypedFormGroup;
types: string[] = [];
radioGroupSubscription: Subscription;
txTypeOptions: IMultiSelectOption[] = [
{ id: 1, name: $localize`Asset listing fee` },
{ id: 2, name: $localize`Blind vote` },
{ id: 3, name: $localize`Compensation request` },
{ id: 4, name: $localize`Genesis` },
{ id: 13, name: $localize`Irregular` },
{ id: 5, name: $localize`Lockup` },
{ id: 6, name: $localize`Pay trade fee` },
{ id: 7, name: $localize`Proof of burn` },
{ id: 8, name: $localize`Proposal` },
{ id: 9, name: $localize`Reimbursement request` },
{ id: 10, name: $localize`Transfer BSQ` },
{ id: 11, name: $localize`Unlock` },
{ id: 12, name: $localize`Vote reveal` },
];
txTypesDefaultChecked = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
txTypeDropdownSettings: IMultiSelectSettings = {
buttonClasses: 'btn btn-primary btn-sm',
displayAllSelectedText: true,
showCheckAll: true,
showUncheckAll: true,
maxHeight: '500px',
fixedTitle: true,
};
txTypeDropdownTexts: IMultiSelectTexts = {
defaultTitle: $localize`:@@bisq-transactions.filter:Filter`,
checkAll: $localize`:@@bisq-transactions.selectall:Select all`,
uncheckAll: $localize`:@@bisq-transactions.unselectall:Unselect all`,
};
// @ts-ignore
paginationSize: 'sm' | 'lg' = 'md';
paginationMaxSize = 5;
txTypes = ['ASSET_LISTING_FEE', 'BLIND_VOTE', 'COMPENSATION_REQUEST', 'GENESIS', 'LOCKUP', 'PAY_TRADE_FEE',
'PROOF_OF_BURN', 'PROPOSAL', 'REIMBURSEMENT_REQUEST', 'TRANSFER_BSQ', 'UNLOCK', 'VOTE_REVEAL', 'IRREGULAR'];
constructor(
private websocketService: WebsocketService,
private bisqApiService: BisqApiService,
private seoService: SeoService,
private formBuilder: UntypedFormBuilder,
private route: ActivatedRoute,
private router: Router,
private cd: ChangeDetectorRef,
) { }
ngOnInit(): void {
this.websocketService.want(['blocks']);
this.seoService.setTitle($localize`:@@add4cd82e3e38a3110fe67b3c7df56e9602644ee:Transactions`);
this.seoService.setDescription($localize`:@@meta.description.bisq.transactions:See recent BSQ transactions: amount, txid, associated Bitcoin block, transaction type, and more.`);
this.radioGroupForm = this.formBuilder.group({
txTypes: [this.txTypesDefaultChecked],
});
this.loadingItems = Array(this.itemsPerPage);
if (document.body.clientWidth < 670) {
this.paginationSize = 'sm';
this.paginationMaxSize = 3;
}
this.transactions$ = this.route.queryParams
.pipe(
tap((queryParams) => {
if (queryParams.page) {
const newPage = parseInt(queryParams.page, 10);
this.page = newPage;
} else {
this.page = 1;
}
if (queryParams.types) {
const types = queryParams.types.split(',').map((str: string) => parseInt(str, 10));
this.types = types.map((id: number) => this.txTypes[id - 1]);
this.radioGroupForm.get('txTypes').setValue(types, { emitEvent: false });
} else {
this.types = [];
this.radioGroupForm.get('txTypes').setValue([], { emitEvent: false });
}
this.cd.markForCheck();
}),
switchMap(() => this.bisqApiService.listTransactions$((this.page - 1) * this.itemsPerPage, this.itemsPerPage, this.types)),
map((response) => [response.body, parseInt(response.headers.get('x-total-count'), 10)])
);
this.radioGroupSubscription = this.radioGroupForm.valueChanges
.subscribe((data) => {
this.types = data.txTypes.map((id: number) => this.txTypes[id - 1]);
if (this.types.length === this.txTypes.length) {
this.types = [];
}
this.page = 1;
this.typesChanged(data.txTypes);
this.cd.markForCheck();
});
}
pageChange(page: number) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { page: page },
queryParamsHandling: 'merge',
});
}
typesChanged(types: number[]) {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { types: types.join(','), page: 1 },
queryParamsHandling: 'merge',
});
}
calculateTotalOutput(outputs: BisqOutput[]): number {
return outputs.reduce((acc: number, output: BisqOutput) => acc + output.bsqAmount, 0);
}
getStringByTxType(type: string) {
const id = this.txTypes.indexOf(type) + 1;
return this.txTypeOptions.find((type) => id === type.id).name;
}
trackByFn(index: number) {
return index;
}
onResize(event: any) {
this.paginationMaxSize = event.target.innerWidth < 670 ? 3 : 5;
}
ngOnDestroy(): void {
this.radioGroupSubscription.unsubscribe();
}
}

View File

@@ -1,83 +0,0 @@
<div class="header-bg box">
<div class="row">
<div class="col">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<tbody>
<ng-template ngFor let-input [ngForOf]="tx.inputs" [ngForTrackBy]="trackByIndexFn">
<tr *ngIf="input.isVerified">
<td class="arrow-td">
<ng-template [ngIf]="input.spendingTxId === null" [ngIfElse]="hasPreoutput">
<span class="grey">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</span>
</ng-template>
<ng-template #hasPreoutput>
<a [routerLink]="['/tx/' | relativeUrl, input.spendingTxId]" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a>
</ng-template>
</td>
<td>
<a [routerLink]="['/address/' | relativeUrl, 'B' + input.address]" title="B{{ input.address }}">
<span class="d-block d-lg-none">B{{ input.address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">B{{ input.address | shortenString : 35 }}</span>
</a>
</td>
<td class="text-right nowrap">
<app-bsq-amount [bsq]="input.bsqAmount"></app-bsq-amount>
</td>
</tr>
</ng-template>
</tbody>
</table>
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col mobile-bottomcol">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<tbody>
<ng-template ngFor let-output [ngForOf]="tx.outputs" [ngForTrackBy]="trackByIndexFn">
<tr *ngIf="output.isVerified && output.opReturn === undefined">
<td>
<a [routerLink]="['/address/' | relativeUrl, 'B' + output.address]" title="B{{ output.address }}">
<span class="d-block d-lg-none">B{{ output.address | shortenString : 16 }}</span>
<span class="d-none d-lg-block">B{{ output.address | shortenString : 35 }}</span>
</a>
</td>
<td class="text-right nowrap">
<app-bsq-amount [bsq]="output.bsqAmount"></app-bsq-amount>
</td>
<td class="arrow-td">
<span *ngIf="!output.spentInfo; else spent" class="green">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</span>
<ng-template #spent>
<a [routerLink]="['/tx/' | relativeUrl, output.spentInfo.txId]" class="red">
<fa-icon [icon]="['fas', 'arrow-alt-circle-right']" [fixedWidth]="true"></fa-icon>
</a>
</ng-template>
</td>
</tr>
</ng-template>
</tbody>
</table>
</div>
</div>
<div>
<div class="transaction-fee" *ngIf="showConfirmations && tx.burntFee">
<ng-container i18n="BSQ burnt amount">Burnt amount</ng-container>: {{ tx.burntFee / 100 | number: '1.2-2' }} <span class="symbol">BSQ</span> <span class="extra-info"><span class="fiat"><app-bsq-amount [bsq]="tx.burntFee" [forceFiat]="true" [green]="true"></app-bsq-amount></span></span>
</div>
<div class="btn-container">
<span *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
<app-confirmations [chainTip]="latestBlock?.height" [height]="tx.blockHeight" [hideUnconfirmed]="true" buttonClass="mt-2"></app-confirmations>
&nbsp;
</span>
<button type="button" class="btn btn-sm btn-primary mt-2" (click)="switchCurrency()">
<app-bsq-amount [bsq]="totalOutput"></app-bsq-amount>
</button>
</div>
<div class="clearfix"></div>
</div>
</div>

View File

@@ -1,103 +0,0 @@
.arrow-td {
width: 20px;
}
.green, .grey, .red {
font-size: 16px;
top: -2px;
position: relative;
@media( min-width: 576px){
font-size: 19px;
}
}
.green {
color:#28a745;
}
.red {
color:#dc3545;
}
.grey {
color:#6c757d;
}
@media (max-width: 767.98px) {
.mobile-bottomcol {
margin-top: 15px;
}
.details-table td:first-child {
white-space: pre-wrap;
}
}
.details-table {
margin-top: 5px;
}
.details-table td {
padding: 0.75rem;
}
.details-table td:nth-child(2) {
word-break: break-all;
white-space: normal;
font-family: "Courier New", Courier, monospace;
font-size: 12px;
}
.smaller-text {
font-size: 12px;
@media (min-width: 576px) {
font-size: 14px !important;
}
}
.longer {
max-width: 100% !important;
width: 200px;
display: inline-block;
}
.row{
flex-direction: column;
@media (min-width: 992px) {
flex-direction: row;
}
}
.extra-info {
display: inline-table;
.fiat {
font-size: 14px;
display: block;
text-align: right;
}
}
.transaction-fee {
display: block;
margin: 0px auto 5px;
@media (min-width: 576px) {
display: inline-table;
}
}
.fiat {
margin-left: 10px;
font-size: 13px;
@media (min-width: 576px) {
font-size: 14px;
}
}
.btn-container {
text-align: right;
@media (min-width: 576px) {
display: inline-table;
float: right;
}
}

View File

@@ -1,42 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core';
import { BisqTransaction } from '../../bisq/bisq.interfaces';
import { StateService } from '../../services/state.service';
import { map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { Block } from '../../interfaces/electrs.interface';
@Component({
selector: 'app-bisq-transfers',
templateUrl: './bisq-transfers.component.html',
styleUrls: ['./bisq-transfers.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class BisqTransfersComponent implements OnInit, OnChanges {
@Input() tx: BisqTransaction;
@Input() showConfirmations = false;
totalOutput: number;
latestBlock$: Observable<Block>;
constructor(
private stateService: StateService,
) { }
trackByIndexFn(index: number) {
return index;
}
ngOnInit() {
this.latestBlock$ = this.stateService.blocks$.pipe(map((blocks) => blocks[0]));
}
ngOnChanges() {
this.totalOutput = this.tx.outputs.filter((output) => output.isVerified).reduce((acc, output) => acc + output.bsqAmount, 0);
}
switchCurrency() {
const oldvalue = !this.stateService.viewFiat$.value;
this.stateService.viewFiat$.next(oldvalue);
}
}

View File

@@ -1,261 +0,0 @@
export interface BisqBlocks {
chainHeight: number;
blocks: BisqBlock[];
}
export interface BisqBlock {
height: number;
time: number;
hash: string;
previousBlockHash: string;
txs: BisqTransaction[];
}
export interface BisqTransaction {
txVersion: string;
id: string;
blockHeight: number;
blockHash: string;
time: number;
inputs: BisqInput[];
outputs: BisqOutput[];
txType: string;
txTypeDisplayString: string;
burntFee: number;
invalidatedBsq: number;
unlockBlockHeight: number;
}
interface BisqInput {
spendingTxOutputIndex: number;
spendingTxId: string;
bsqAmount: number;
isVerified: boolean;
address: string;
time: number;
}
export interface BisqOutput {
txVersion: string;
txId: string;
index: number;
bsqAmount: number;
btcAmount: number;
height: number;
isVerified: boolean;
burntFee: number;
invalidatedBsq: number;
address: string;
scriptPubKey: BisqScriptPubKey;
spentInfo?: SpentInfo;
time: any;
txType: string;
txTypeDisplayString: string;
txOutputType: string;
txOutputTypeDisplayString: string;
lockTime: number;
isUnspent: boolean;
opReturn?: string;
}
export interface BisqStats {
minted: number;
burnt: number;
addresses: number;
unspent_txos: number;
spent_txos: number;
}
interface BisqScriptPubKey {
addresses: string[];
asm: string;
hex: string;
reqSigs?: number;
type: string;
}
interface SpentInfo {
height: number;
inputIndex: number;
txId: string;
}
export interface BisqTrade {
direction: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
market?: string;
}
export interface Currencies { [txid: string]: Currency; }
export interface Currency {
code: string;
name: string;
precision: number;
_type: string;
}
export interface Depth { [market: string]: Market; }
interface Market {
'buys': string[];
'sells': string[];
}
export interface HighLowOpenClose {
period_start: number | string;
open: string;
high: string;
low: string;
close: string;
volume_left: string;
volume_right: string;
avg: string;
}
export interface Markets { [txid: string]: Pair; }
interface Pair {
pair: string;
lname: string;
rname: string;
lsymbol: string;
rsymbol: string;
lprecision: number;
rprecision: number;
ltype: string;
rtype: string;
name: string;
}
export interface Offers { [market: string]: OffersMarket; }
export interface OffersMarket {
buys: Offer[] | null;
sells: Offer[] | null;
}
export interface OffersData {
direction: string;
currencyCode: string;
minAmount: number;
amount: number;
price: number;
date: number;
useMarketBasedPrice: boolean;
marketPriceMargin: number;
paymentMethod: string;
id: string;
currencyPair: string;
primaryMarketDirection: string;
priceDisplayString: string;
primaryMarketAmountDisplayString: string;
primaryMarketMinAmountDisplayString: string;
primaryMarketVolumeDisplayString: string;
primaryMarketMinVolumeDisplayString: string;
primaryMarketPrice: number;
primaryMarketAmount: number;
primaryMarketMinAmount: number;
primaryMarketVolume: number;
primaryMarketMinVolume: number;
}
export interface Offer {
offer_id: string;
offer_date: number;
direction: string;
min_amount: string;
amount: string;
price: string;
volume: string;
payment_method: string;
offer_fee_txid: any;
}
export interface Tickers { [market: string]: Ticker | null; }
export interface Ticker {
last: string;
high: string;
low: string;
volume_left: string;
volume_right: string;
buy: string | null;
sell: string | null;
}
export interface Trade {
market?: string;
price: string;
amount: string;
volume: string;
payment_method: string;
trade_id: string;
trade_date: number;
_market: Pair;
}
export interface TradesData {
currency: string;
direction: string;
tradePrice: number;
tradeAmount: number;
tradeDate: number;
paymentMethod: string;
offerDate: number;
useMarketBasedPrice: boolean;
marketPriceMargin: number;
offerAmount: number;
offerMinAmount: number;
offerId: string;
depositTxId?: string;
currencyPair: string;
primaryMarketDirection: string;
primaryMarketTradePrice: number;
primaryMarketTradeAmount: number;
primaryMarketTradeVolume: number;
_market: string;
_tradePriceStr: string;
_tradeAmountStr: string;
_tradeVolumeStr: string;
_offerAmountStr: string;
_tradePrice: number;
_tradeAmount: number;
_tradeVolume: number;
_offerAmount: number;
}
export interface MarketVolume {
period_start: number;
num_trades: number;
volume: string;
}
export interface MarketsApiError {
success: number;
error: string;
}
export type Interval = 'minute' | 'half_hour' | 'hour' | 'half_day' | 'day' | 'week' | 'month' | 'year' | 'auto';
export interface SummarizedIntervals { [market: string]: SummarizedInterval; }
export interface SummarizedInterval {
period_start: number;
open: number;
close: number;
high: number;
low: number;
avg: number;
volume_right: number;
volume_left: number;
time?: number;
}

View File

@@ -1,84 +0,0 @@
import { NgModule } from '@angular/core';
import { BisqRoutingModule } from './bisq.routing.module';
import { SharedModule } from '../shared/shared.module';
import { LightweightChartsComponent } from './lightweight-charts/lightweight-charts.component';
import { LightweightChartsAreaComponent } from './lightweight-charts-area/lightweight-charts-area.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
import { BisqIconComponent } from './bisq-icon/bisq-icon.component';
import { BisqTransactionDetailsComponent } from './bisq-transaction-details/bisq-transaction-details.component';
import { BisqTransfersComponent } from './bisq-transfers/bisq-transfers.component';
import { FontAwesomeModule, FaIconLibrary } from '@fortawesome/angular-fontawesome';
import { faLeaf, faQuestion, faExclamationTriangle, faRocket, faRetweet, faFileAlt, faMoneyBill,
faEye, faEyeSlash, faLock, faLockOpen, faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
import { BisqApiService } from './bisq-api.service';
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BsqAmountComponent } from './bsq-amount/bsq-amount.component';
import { BisqTradesComponent } from './bisq-trades/bisq-trades.component';
import { CommonModule } from '@angular/common';
import { AutofocusDirective } from '../components/ngx-bootstrap-multiselect/autofocus.directive';
import { MultiSelectSearchFilter } from '../components/ngx-bootstrap-multiselect/search-filter.pipe';
import { OffClickDirective } from '../components/ngx-bootstrap-multiselect/off-click.directive';
import { NgxDropdownMultiselectComponent } from '../components/ngx-bootstrap-multiselect/ngx-bootstrap-multiselect.component';
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
@NgModule({
declarations: [
BisqMasterPageComponent,
BisqTransactionsComponent,
BisqTransactionComponent,
BisqBlockComponent,
BisqTransactionComponent,
BisqIconComponent,
BisqTransactionDetailsComponent,
BisqTransfersComponent,
BisqBlocksComponent,
BisqAddressComponent,
BisqStatsComponent,
BsqAmountComponent,
LightweightChartsComponent,
LightweightChartsAreaComponent,
BisqDashboardComponent,
BisqMarketComponent,
BisqTradesComponent,
BisqMainDashboardComponent,
NgxDropdownMultiselectComponent,
AutofocusDirective,
OffClickDirective,
],
imports: [
CommonModule,
BisqRoutingModule,
SharedModule,
FontAwesomeModule,
],
providers: [
BisqApiService,
MultiSelectSearchFilter,
AutofocusDirective,
OffClickDirective,
]
})
export class BisqModule {
constructor(library: FaIconLibrary) {
library.addIcons(faQuestion);
library.addIcons(faExclamationCircle);
library.addIcons(faExclamationTriangle);
library.addIcons(faRocket);
library.addIcons(faRetweet);
library.addIcons(faLeaf);
library.addIcons(faFileAlt);
library.addIcons(faMoneyBill);
library.addIcons(faEye);
library.addIcons(faEyeSlash);
library.addIcons(faLock);
library.addIcons(faLockOpen);
}
}

View File

@@ -1,95 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component';
import { BisqTransactionsComponent } from './bisq-transactions/bisq-transactions.component';
import { BisqTransactionComponent } from './bisq-transaction/bisq-transaction.component';
import { BisqBlockComponent } from './bisq-block/bisq-block.component';
import { BisqBlocksComponent } from './bisq-blocks/bisq-blocks.component';
import { BisqAddressComponent } from './bisq-address/bisq-address.component';
import { BisqStatsComponent } from './bisq-stats/bisq-stats.component';
import { BisqDashboardComponent } from './bisq-dashboard/bisq-dashboard.component';
import { BisqMarketComponent } from './bisq-market/bisq-market.component';
import { BisqMainDashboardComponent } from './bisq-main-dashboard/bisq-main-dashboard.component';
import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component';
const routes: Routes = [
{
path: '',
component: BisqMasterPageComponent,
children: [
{
path: '',
component: BisqMainDashboardComponent,
},
{
path: 'markets',
data: { networks: ['bisq'] },
component: BisqDashboardComponent,
},
{
path: 'transactions',
data: { networks: ['bisq'] },
component: BisqTransactionsComponent
},
{
path: 'market/:pair',
data: { networkSpecific: true },
component: BisqMarketComponent,
},
{
path: 'tx/push',
component: PushTransactionComponent,
},
{
path: 'tx/:id',
data: { networkSpecific: true },
component: BisqTransactionComponent
},
{
path: 'blocks',
children: [],
component: BisqBlocksComponent
},
{
path: 'block/:id',
data: { networkSpecific: true },
component: BisqBlockComponent,
},
{
path: 'address/:id',
data: { networkSpecific: true },
component: BisqAddressComponent,
},
{
path: 'stats',
data: { networks: ['bisq'] },
component: BisqStatsComponent,
},
{
path: 'about',
loadChildren: () => import('../components/about/about.module').then(m => m.AboutModule),
},
{
path: 'docs',
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
},
{
path: 'api',
loadChildren: () => import('../docs/docs.module').then(m => m.DocsModule)
},
{
path: 'terms-of-service',
loadChildren: () => import('../components/terms-of-service/terms-of-service.module').then(m => m.TermsOfServiceModule),
},
{
path: '**',
redirectTo: ''
}
]
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class BisqRoutingModule { }

View File

@@ -1,6 +0,0 @@
<ng-container *ngIf="(forceFiat || (viewFiat$ | async)) && (conversions$ | async) as conversions; else viewFiatVin">
<span [class.green-color]="green">{{ conversions.USD * bsq / 100 * (bsqPrice$ | async) / 100000000 | currency:'USD':'symbol':'1.2-2' }}</span>
</ng-container>
<ng-template #viewFiatVin>
{{ bsq / 100 | number : digitsInfo }} <span class="symbol">BSQ</span>
</ng-template>

View File

@@ -1,3 +0,0 @@
.green-color {
color: #3bcc49;
}

View File

@@ -1,30 +0,0 @@
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
import { StateService } from '../../services/state.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-bsq-amount',
templateUrl: './bsq-amount.component.html',
styleUrls: ['./bsq-amount.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BsqAmountComponent implements OnInit {
conversions$: Observable<any>;
viewFiat$: Observable<boolean>;
bsqPrice$: Observable<number>;
@Input() bsq: number;
@Input() digitsInfo = '1.2-2';
@Input() forceFiat = false;
@Input() green = false;
constructor(
private stateService: StateService,
) { }
ngOnInit() {
this.viewFiat$ = this.stateService.viewFiat$.asObservable();
this.conversions$ = this.stateService.conversions$.asObservable();
this.bsqPrice$ = this.stateService.bsqPrice$;
}
}

View File

@@ -1,25 +0,0 @@
:host ::ng-deep .floating-tooltip-2 {
width: 160px;
height: 80px;
position: absolute;
display: none;
padding: 8px;
box-sizing: border-box;
font-size: 12px;
color:rgba(255, 255, 255, 1);
background-color: #131722;
text-align: left;
z-index: 1000;
top: 12px;
left: 12px;
pointer-events: none;
border-radius: 2px;
}
:host ::ng-deep .volumeText {
color: rgba(33, 150, 243, 0.7);
}
:host ::ng-deep .tradesText {
color: rgba(37, 177, 53, 1);
}

View File

@@ -1,151 +0,0 @@
import { createChart, CrosshairMode, isBusinessDay } from 'lightweight-charts';
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-lightweight-charts-area',
template: '<ng-component></ng-component>',
styleUrls: ['./lightweight-charts-area.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LightweightChartsAreaComponent implements OnInit, OnChanges, OnDestroy {
@Input() data: any;
@Input() lineData: any;
@Input() precision: number;
@Input() height = 500;
areaSeries: any;
volumeSeries: any;
chart: any;
lineSeries: any;
container: any;
width: number;
constructor(
private element: ElementRef,
) { }
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
this.width = this.element.nativeElement.parentElement.offsetWidth;
this.chart.applyOptions({
width: this.width,
height: this.height,
});
}
ngOnInit() {
this.width = this.element.nativeElement.parentElement.offsetWidth;
this.container = document.createElement('div');
const chartholder = this.element.nativeElement.appendChild(this.container);
this.chart = createChart(chartholder, {
width: this.width,
height: this.height,
crosshair: {
mode: CrosshairMode.Normal,
},
layout: {
backgroundColor: '#000',
textColor: 'rgba(255, 255, 255, 0.8)',
},
grid: {
vertLines: {
color: 'rgba(255, 255, 255, 0.1)',
},
horzLines: {
color: 'rgba(255, 255, 255, 0.1)',
},
},
rightPriceScale: {
borderColor: 'rgba(255, 255, 255, 0.2)',
},
timeScale: {
borderColor: 'rgba(255, 255, 255, 0.2)',
},
});
this.lineSeries = this.chart.addLineSeries({
color: 'rgba(37, 177, 53, 1)',
lineColor: 'rgba(216, 27, 96, 1)',
lineWidth: 2,
});
this.areaSeries = this.chart.addAreaSeries({
topColor: 'rgba(33, 150, 243, 0.7)',
bottomColor: 'rgba(33, 150, 243, 0.1)',
lineColor: 'rgba(33, 150, 243, 0.1)',
lineWidth: 2,
});
const toolTip = document.createElement('div');
toolTip.className = 'floating-tooltip-2';
chartholder.appendChild(toolTip);
this.chart.subscribeCrosshairMove((param) => {
if (!param.time || param.point.x < 0 || param.point.x > this.width || param.point.y < 0 || param.point.y > this.height) {
toolTip.style.display = 'none';
return;
}
const dateStr = isBusinessDay(param.time)
? this.businessDayToString(param.time)
: new Date(param.time * 1000).toLocaleDateString();
toolTip.style.display = 'block';
const price = param.seriesPrices.get(this.areaSeries);
const line = param.seriesPrices.get(this.lineSeries);
const tradesText = $localize`:@@bisq-graph-trades:Trades`;
const volumeText = $localize`:@@bisq-graph-volume:Volume`;
toolTip.innerHTML = `<table>
<tr><td class="tradesText">${tradesText}:</td><td class="text-right tradesText">${Math.round(line * 100) / 100}</td></tr>
<tr><td class="volumeText">${volumeText}:<td class="text-right volumeText">${Math.round(price * 100) / 100} BTC</td></tr>
</table>
<div>${dateStr}</div>`;
const y = param.point.y;
const toolTipWidth = 100;
const toolTipHeight = 80;
const toolTipMargin = 15;
let left = param.point.x + toolTipMargin;
if (left > this.width - toolTipWidth) {
left = param.point.x - toolTipMargin - toolTipWidth;
}
let top = y + toolTipMargin;
if (top > this.height - toolTipHeight) {
top = y - toolTipHeight - toolTipMargin;
}
toolTip.style.left = left + 'px';
toolTip.style.top = top + 'px';
});
this.updateData();
}
businessDayToString(businessDay) {
return businessDay.year + '-' + businessDay.month + '-' + businessDay.day;
}
ngOnChanges(changes: SimpleChanges) {
if (!changes.data || changes.data.isFirstChange()){
return;
}
this.updateData();
}
updateData() {
this.areaSeries.setData(this.data);
this.lineSeries.setData(this.lineData);
}
ngOnDestroy() {
this.chart.remove();
}
}

View File

@@ -1,94 +0,0 @@
import { createChart, CrosshairMode } from 'lightweight-charts';
import { ChangeDetectionStrategy, Component, ElementRef, HostListener, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-lightweight-charts',
template: '<ng-component></ng-component>',
styleUrls: ['./lightweight-charts.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LightweightChartsComponent implements OnInit, OnChanges, OnDestroy {
@Input() data: any;
@Input() volumeData: any;
@Input() precision: number;
@Input() height = 500;
lineSeries: any;
volumeSeries: any;
chart: any;
constructor(
private element: ElementRef,
) { }
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
this.chart.applyOptions({
width: this.element.nativeElement.parentElement.offsetWidth,
height: this.height,
});
}
ngOnInit() {
this.chart = createChart(this.element.nativeElement, {
width: this.element.nativeElement.parentElement.offsetWidth,
height: this.height,
layout: {
backgroundColor: '#000000',
textColor: '#d1d4dc',
},
crosshair: {
mode: CrosshairMode.Normal,
},
grid: {
vertLines: {
visible: true,
color: 'rgba(42, 46, 57, 0.5)',
},
horzLines: {
color: 'rgba(42, 46, 57, 0.5)',
},
},
});
this.lineSeries = this.chart.addCandlestickSeries();
this.volumeSeries = this.chart.addHistogramSeries({
color: '#26a69a',
priceFormat: {
type: 'volume',
},
priceScaleId: '',
scaleMargins: {
top: 0.85,
bottom: 0,
},
});
this.updateData();
}
ngOnChanges(changes: SimpleChanges) {
if (!changes.data || changes.data.isFirstChange()){
return;
}
this.updateData();
}
ngOnDestroy() {
this.chart.remove();
}
updateData() {
this.lineSeries.setData(this.data);
this.volumeSeries.setData(this.volumeData);
this.lineSeries.applyOptions({
priceFormat: {
type: 'price',
precision: this.precision,
minMove: 0.0000001,
},
});
}
}

View File

@@ -45,10 +45,10 @@
</form>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnDestroy, OnInit } from '@angular/core';
import { EChartsOption } from 'echarts';
import { EChartsOption } from '../../../graphs/echarts';
import { Observable, Subscription, combineLatest, fromEvent, share } from 'rxjs';
import { startWith, switchMap, tap } from 'rxjs/operators';
import { SeoService } from '../../../services/seo.service';
@@ -11,6 +11,7 @@ import { MiningService } from '../../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { Acceleration } from '../../../interfaces/node-api.interface';
import { ServicesApiServices } from '../../../services/services-api.service';
import { StateService } from '../../../services/state.service';
@Component({
selector: 'app-acceleration-fees-graph',
@@ -59,6 +60,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
private storageService: StorageService,
private miningService: MiningService,
private route: ActivatedRoute,
public stateService: StateService,
private cd: ChangeDetectorRef,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });
@@ -154,7 +156,7 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
let tooltip = `<b style="color: white; margin-left: 2px">${formatterXAxis(this.locale, this.timespan, parseInt(ticks[0].axisValue, 10))}</b><br>`;
if (ticks[0].data[1] > 10_000_000) {
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-0')} BTC<br>`;
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1] / 100_000_000, this.locale, '1.0-8')} BTC<br>`;
} else {
tooltip += `${ticks[0].marker} ${ticks[0].seriesName}: ${formatNumber(ticks[0].data[1], this.locale, '1.0-0')} sats<br>`;
}
@@ -176,10 +178,10 @@ export class AccelerationFeesGraphComponent implements OnInit, OnDestroy {
padding: [10, 0, 0, 0],
},
type: 'time',
boundaryGap: false,
boundaryGap: [0, 0],
axisLine: { onZero: true },
axisLabel: {
formatter: val => formatterXAxisTimeCategory(this.locale, this.timespan, parseInt(val, 10)),
formatter: (val): string => formatterXAxisTimeCategory(this.locale, this.timespan, val),
align: 'center',
fontSize: 11,
lineHeight: 12,

View File

@@ -42,7 +42,7 @@
<span>&nbsp;</span>
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: 'text-top'; font-size: 13px; color: #4a68b9"></fa-icon>
</a>
<div class="mempool-block-wrapper">
<div class="mempool-block-wrapper" *ngIf="webGlEnabled">
<app-mempool-block-overview [index]="0" [overrideColors]="getAcceleratorColor"></app-mempool-block-overview>
</div>
</div>
@@ -66,16 +66,6 @@
</div>
</div>
<!-- Avg block fees graph -->
<!-- <div class="col" style="margin-bottom: 1.47rem">
<div class="card">
<div class="card-body pl-lg-3 pr-lg-3 pl-2 pr-2">
<app-block-fee-rates-graph [attr.data-cy]="'hashrate-graph'" [widget]="true"></app-block-fee-rates-graph>
<div class="mt-1"><a [routerLink]="['/graphs/mining/block-fee-rates' | relativeUrl]" fragment="1m" i18n="dashboard.view-more">View more &raquo;</a></div>
</div>
</div>
</div> -->
<!-- Recent Accelerations List -->
<div class="col">
<div class="card list-card">

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, HostListener, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core';
import { SeoService } from '../../../services/seo.service';
import { OpenGraphService } from '../../../services/opengraph.service';
import { WebsocketService } from '../../../services/websocket.service';
@@ -10,6 +10,7 @@ import { hexToColor } from '../../block-overview-graph/utils';
import TxView from '../../block-overview-graph/tx-view';
import { feeLevels, mempoolFeeColors } from '../../../app.constants';
import { ServicesApiServices } from '../../../services/services-api.service';
import { detectWebGL } from '../../../shared/graphs.utils';
const acceleratedColor: Color = hexToColor('8F5FF6');
const normalColors = mempoolFeeColors.map(hex => hexToColor(hex + '5F'));
@@ -30,6 +31,7 @@ export class AcceleratorDashboardComponent implements OnInit {
pendingAccelerations$: Observable<Acceleration[]>;
minedAccelerations$: Observable<Acceleration[]>;
loadingBlocks: boolean = true;
webGlEnabled = true;
graphHeight: number = 300;
@@ -39,8 +41,10 @@ export class AcceleratorDashboardComponent implements OnInit {
private websocketService: WebsocketService,
private serviceApiServices: ServicesApiServices,
private stateService: StateService,
@Inject(PLATFORM_ID) private platformId: Object,
) {
this.seoService.setTitle($localize`:@@a681a4e2011bb28157689dbaa387de0dd0aa0c11:Accelerator Dashboard`);
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.seoService.setTitle($localize`:@@6b867dc61c6a92f3229f1950f9f2d414790cce95:Accelerator Dashboard`);
this.ogService.setManualOgImage('accelerator.jpg');
}
@@ -48,7 +52,7 @@ export class AcceleratorDashboardComponent implements OnInit {
this.onResize();
this.websocketService.want(['blocks', 'mempool-blocks', 'stats']);
this.pendingAccelerations$ = interval(30000).pipe(
this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe(
startWith(true),
switchMap(() => {
return this.serviceApiServices.getAccelerations$().pipe(

View File

@@ -8,7 +8,7 @@
</div>
<ng-container *ngIf="!error">
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">

View File

@@ -1,10 +1,11 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, SimpleChanges } from '@angular/core';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { of } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ChainStats } from '../../interfaces/electrs.interface';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router';
@Component({
selector: 'app-address-graph',
@@ -20,13 +21,16 @@ import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
`],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddressGraphComponent implements OnInit, OnChanges {
export class AddressGraphComponent implements OnChanges {
@Input() address: string;
@Input() isPubkey: boolean = false;
@Input() stats: ChainStats;
@Input() right: number | string = 10;
@Input() left: number | string = 70;
data: any[] = [];
hoverData: any[] = [];
chartOptions: EChartsOption = {};
chartInitOptions = {
renderer: 'svg',
@@ -39,14 +43,10 @@ export class AddressGraphComponent implements OnInit, OnChanges {
constructor(
@Inject(LOCALE_ID) public locale: string,
private electrsApiService: ElectrsApiService,
private router: Router,
private amountShortenerPipe: AmountShortenerPipe,
private cd: ChangeDetectorRef,
) {
}
ngOnInit(): void {
}
) {}
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
@@ -69,13 +69,13 @@ export class AddressGraphComponent implements OnInit, OnChanges {
prepareChartOptions(summary): void {
let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); // + (summary[0]?.value || 0);
const data = summary.map(d => {
this.data = summary.map(d => {
const balance = total;
total -= d.value;
return [d.time * 1000, balance, d];
}).reverse();
const maxValue = data.reduce((acc, d) => Math.max(acc, Math.abs(d[1])), 0);
const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1])), 0);
this.chartOptions = {
color: [
@@ -163,10 +163,11 @@ export class AddressGraphComponent implements OnInit, OnChanges {
showSymbol: false,
symbol: 'circle',
symbolSize: 8,
data: data,
data: this.data,
areaStyle: {
opacity: 0.5,
},
triggerLineEvent: true,
type: 'line',
smooth: false,
step: 'end'
@@ -175,8 +176,20 @@ export class AddressGraphComponent implements OnInit, OnChanges {
};
}
onChartClick(e) {
if (this.hoverData?.length && this.hoverData[0]?.[2]?.txid) {
this.router.navigate(['/tx/', this.hoverData[0][2].txid]);
}
}
onTooltip(e) {
this.hoverData = (e?.dataByCoordSys?.[0]?.dataByAxis?.[0]?.seriesDataIndices || []).map(indices => this.data[indices.dataIndex]);
}
onChartInit(ec) {
this.chartInstance = ec;
this.chartInstance.on('showTip', this.onTooltip.bind(this));
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
}
isMobile() {

View File

@@ -69,7 +69,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.addressString = this.addressString.toLowerCase();
}
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'} ${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${this.addressString}:INTERPOLATION:.`);
return (this.addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
? this.electrsApiService.getPubKeyAddress$(this.addressString)

View File

@@ -51,7 +51,7 @@
</div>
</div>
<ng-container *ngIf="address && transactions && transactions.length > 2">
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
<br>
<div class="box">
<div class="row">
@@ -135,11 +135,10 @@
<ng-template [ngIf]="error">
<br>
<div class="text-center">
<span i18n="address.error.loading-address-data">Error loading address data.</span>
<br>
<ng-template #displayServerError><i class="small">({{ error.error }})</i></ng-template>
<ng-template [ngIf]="error.status === 413 || error.status === 405 || error.status === 504" [ngIfElse]="displayServerError">
<ng-template [ngIf]="error.status === 413 || error.status === 405 || error.status === 504" [ngIfElse]="displayServerError">
<div class="text-center">
<span i18n="address.error.loading-address-data">Error loading address data.</span>
<br>
<ng-container i18n="Electrum server limit exceeded error">
<i>There many transactions on this address, more than your backend can handle. See more on <a href="/docs/faq#address-lookup-issues">setting up a stronger backend</a>.</i>
<br><br>
@@ -150,9 +149,14 @@
<br>
<a href="http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/address/{{ addressString }}" target="_blank">http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/address/{{ addressString }}</a>
<br><br>
<i class="small">({{ error.error }})</i>
</ng-template>
</div>
<i class="small">({{ error | httpErrorMsg }})</i>
</div>
</ng-template>
<ng-template #displayServerError>
<app-http-error [error]="error">
<span i18n="address.error.loading-address-data">Error loading address data.</span>
</app-http-error>
</ng-template>
</ng-template>
</div>

View File

@@ -44,7 +44,7 @@ export class AddressComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private electrsApiService: ElectrsApiService,
private websocketService: WebsocketService,
private stateService: StateService,
public stateService: StateService,
private audioService: AudioService,
private apiService: ApiService,
private seoService: SeoService,
@@ -140,10 +140,22 @@ export class AddressComponent implements OnInit, OnDestroy {
if (!fetchTxs.length) {
return of([]);
}
return this.apiService.getTransactionTimes$(fetchTxs);
return this.apiService.getTransactionTimes$(fetchTxs).pipe(
catchError((err) => {
this.isLoadingAddress = false;
this.isLoadingTransactions = false;
this.error = err;
this.seoService.logSoft404();
console.log(err);
return of([]);
})
);
})
)
.subscribe((times: number[]) => {
.subscribe((times: number[] | null) => {
if (!times) {
return;
}
times.forEach((time, index) => {
this.tempTransactions[this.timeTxIndexes[index]].firstSeen = time;
});

View File

@@ -55,8 +55,6 @@ export class AppComponent implements OnInit {
let domain = 'mempool.space';
if (this.stateService.env.BASE_MODULE === 'liquid') {
domain = 'liquid.network';
} else if (this.stateService.env.BASE_MODULE === 'bisq') {
domain = 'bisq.markets';
}
this.link.setAttribute('href', 'https://' + domain + this.location.path());
}

View File

@@ -146,13 +146,10 @@
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
<app-http-error [error]="error">
<span i18n="asset.error.loading-asset-data">Error loading asset data.</span>
<br>
<i>{{ error.error }}</i>
</div>
</app-http-error>
</ng-template>
</div>
<br>

View File

@@ -105,6 +105,7 @@ export class AssetComponent implements OnInit, OnDestroy {
if (!this.assetContract) {
this.assetContract = [null, '?', 'Unknown', 0];
}
this.seoService.setDescription($localize`:@@meta.description.liquid.asset:Browse an overview of the Liquid asset ${this.assetContract[2]}:INTERPOLATION: (${this.assetContract[1]}:INTERPOLATION:): see issued amount, burned amount, circulating amount, related transactions, and more.`);
this.blindedIssuance = this.asset.chain_stats.has_blinded_issuances || this.asset.mempool_stats.has_blinded_issuances;
this.isNativeAsset = asset.asset_id === this.nativeAssetId;
this.updateChainStats();

View File

@@ -1,5 +1,7 @@
<div *ngIf="featuredAssets$ | async as featured; else loading" class="featuredBox">
<div *ngIf="featured.length === 0" class="text-center">
<div i18n="liquid.no-featured.assets" class="symbol">No featured assets</div>
</div>
<div class="card" *ngFor="let group of featured">
<ng-template [ngIf]="group.assets" [ngIfElse]="singleAsset">
<a [routerLink]="['/assets/group' | relativeUrl, group.id]">

View File

@@ -47,3 +47,9 @@
.ticker {
color: grey;
}
.symbol {
color: grey;
font-size: 1.5rem;
font-style: italic;
}

View File

@@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiService } from '../../../services/api.service';
import { StateService } from '../../../services/state.service';
@Component({
selector: 'app-assets-featured',
@@ -12,10 +13,11 @@ export class AssetsFeaturedComponent implements OnInit {
constructor(
private apiService: ApiService,
private stateService: StateService,
) { }
ngOnInit(): void {
this.featuredAssets$ = this.apiService.listFeaturedAssets$();
this.featuredAssets$ = this.apiService.listFeaturedAssets$(this.stateService.network);
}
}

View File

@@ -46,9 +46,7 @@
</ng-template>
<ng-template [ngIf]="error">
<div class="text-center">
<ng-container i18n="Asset data load error">Error loading assets data.</ng-container>
<br>
<i>{{ error.error }}</i>
</div>
<app-http-error [error]="error">
<span i18n="Asset data load error">Error loading assets data.</span>
</app-http-error>
</ng-template>

View File

@@ -1,91 +0,0 @@
<header class="sticky-header">
<nav class="navbar navbar-expand-md navbar-dark bg-dark">
<a class="navbar-brand" [routerLink]="['/' | relativeUrl]" style="position: relative;">
<ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
<div height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }">
<svg width="140" viewBox="0 0 280 71" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M68.137 62.1803C68.137 66.8789 64.3484 70.6717 59.6552 70.6717H8.48178C3.78853 70.6717 0 66.8789 0 62.1803V10.9485C0 6.24988 3.8168 2.45703 8.48178 2.45703H59.6552C64.3484 2.45703 68.137 6.24988 68.137 10.9485V62.1803Z" fill="#2E3349"/>
<path d="M0 36.6504V62.1814C0 66.88 3.8168 70.6728 8.51005 70.6728H59.6552C64.3484 70.6728 68.1652 66.88 68.1652 62.1814V36.6504H0Z" fill="url(#paint0_linear)"/>
<path opacity="0.3" d="M60.054 61.5586C60.054 62.6059 59.3472 63.455 58.4707 63.455H49.6497C48.7732 63.455 48.0664 62.6059 48.0664 61.5586V11.5722C48.0664 10.5249 48.7732 9.67578 49.6497 9.67578H58.4707C59.3472 9.67578 60.054 10.5249 60.054 11.5722V61.5586Z" fill="white"/>
<path d="M85.4102 33.8734H89.6242V30.6894H89.7179C91.3567 33.0774 94.447 34.4352 97.4437 34.4352C104.327 34.4352 108.728 29.3315 108.728 22.7763C108.728 16.1274 104.28 11.1173 97.4437 11.1173C94.2597 11.1173 91.2162 12.5688 89.7179 14.8632H89.6242V3.84819H85.4102V33.8734ZM96.9287 30.5021C92.4336 30.5021 89.6242 27.2713 89.6242 22.7763C89.6242 18.2812 92.4336 15.0504 96.9287 15.0504C101.424 15.0504 104.233 18.2812 104.233 22.7763C104.233 27.2713 101.424 30.5021 96.9287 30.5021Z" fill="white"/>
<path d="M112.059 33.8734H116.274V11.6792H112.059V33.8734ZM111.076 3.71923C111.076 5.40487 112.481 6.80957 114.167 6.80957C115.852 6.80957 117.257 5.40487 117.257 3.71923C117.257 2.0336 115.852 0.628906 114.167 0.628906C112.481 0.628906 111.076 2.0336 111.076 3.71923Z" fill="white"/>
<path d="M136.522 14.7695C134.93 12.1474 131.887 11.1173 128.937 11.1173C124.769 11.1173 120.509 13.318 120.509 17.9535C120.509 22.2144 123.693 23.385 127.298 24.2746C129.124 24.696 132.636 25.1643 132.636 27.6927C132.636 29.6125 130.295 30.5021 128.141 30.5021C125.706 30.5021 124.114 29.2379 122.756 27.88L119.572 30.5021C121.773 33.4988 124.489 34.4352 128.141 34.4352C132.542 34.4352 137.131 32.4687 137.131 27.4586C137.131 23.2913 134.321 21.8866 130.669 20.997C128.796 20.5756 125.004 20.201 125.004 17.5321C125.004 15.9401 126.736 15.0504 128.703 15.0504C130.81 15.0504 132.261 16.0337 133.244 17.2511L136.522 14.7695Z" fill="white"/>
<path d="M162.956 11.6792H158.742V14.8632H158.648C157.01 12.4752 153.919 11.1173 150.923 11.1173C144.04 11.1173 139.638 16.221 139.638 22.7763C139.638 29.4252 144.086 34.4352 150.923 34.4352C154.107 34.4352 157.15 32.9837 158.648 30.6894H158.742V39.9435H162.956V11.6792ZM151.438 15.0504C155.933 15.0504 158.742 18.2812 158.742 22.7763C158.742 27.2713 155.933 30.5021 151.438 30.5021C146.943 30.5021 144.133 27.2713 144.133 22.7763C144.133 18.2812 146.943 15.0504 151.438 15.0504Z" fill="white"/>
<path d="M84.8989 66.394C86.5846 66.394 87.9893 64.9893 87.9893 63.3037C87.9893 61.6181 86.5846 60.2134 84.8989 60.2134C83.2133 60.2134 81.8086 61.6181 81.8086 63.3037C81.8086 64.9893 83.2133 66.394 84.8989 66.394Z" fill="#25B135"/>
<path d="M94.6063 66.1131H98.8204V54.5946C98.8204 49.5845 101.536 47.2902 104.58 47.2902C108.653 47.2902 109.262 50.2869 109.262 54.5009V66.1131H113.476V53.9859C113.476 50.0527 115.068 47.2902 119.142 47.2902C123.215 47.2902 123.918 50.3805 123.918 53.7518V66.1131H128.132V53.1899C128.132 48.2266 126.54 43.357 119.704 43.357C117.035 43.357 114.132 44.7617 112.68 47.4775C111.275 44.7617 109.028 43.357 105.75 43.357C101.77 43.357 99.0545 46.0728 98.6331 47.3838H98.5395V43.9189H94.6063V66.1131Z" fill="#25B135"/>
<path d="M135.566 49.2567C137.111 48.0862 138.656 46.7283 141.887 46.7283C145.493 46.7283 147.178 49.1163 147.178 51.4106V52.3471H144.088C137.345 52.3471 131.867 54.3137 131.867 60.0261C131.867 64.3338 135.426 66.675 139.546 66.675C142.917 66.675 145.446 65.598 147.319 62.7418H147.412C147.412 63.8656 147.459 64.9893 147.553 66.1131H151.299C151.158 64.9425 151.111 63.6315 151.111 62.0863V50.7551C151.111 46.9156 148.396 43.357 141.84 43.357C138.75 43.357 135.379 44.434 133.038 46.6346L135.566 49.2567ZM147.178 55.4374V56.8421C147.178 59.8388 145.539 63.3037 140.857 63.3037C137.954 63.3037 136.081 62.2268 136.081 59.6983C136.081 56.1398 140.951 55.4374 144.931 55.4374H147.178Z" fill="#25B135"/>
<path d="M155.689 66.1131H159.903V54.9692C159.903 50.0996 162.151 47.8521 166.271 47.8521C166.927 47.8521 167.629 47.9925 168.331 48.1798L168.519 43.638C167.957 43.4507 167.301 43.357 166.646 43.357C163.883 43.357 161.074 44.9958 159.997 47.337H159.903V43.9189H155.689V66.1131Z" fill="#25B135"/>
<path d="M171.484 66.1131H175.698V54.5946L185.999 66.1131H191.993L180.755 54.0327L191.103 43.9657H185.25L175.698 53.5645V34.5527H171.484V66.1131Z" fill="#25B135"/>
<path d="M215.206 56.5612V55.0628C215.206 49.3504 212.209 43.357 204.39 43.357C197.741 43.357 192.918 48.3671 192.918 55.016C192.918 61.6181 197.32 66.675 204.343 66.675C208.604 66.675 211.835 65.1766 214.176 62.1331L210.992 59.6983C209.353 61.7117 207.48 63.0228 204.905 63.0228C201.019 63.0228 197.413 60.4475 197.413 56.5612H215.206ZM197.413 53.1899C197.413 50.24 200.129 46.7283 204.296 46.7283C208.557 46.7283 210.617 49.4909 210.711 53.1899H197.413Z" fill="#25B135"/>
<path d="M232.14 43.9189H226.1V37.6914H221.886V43.9189H217.016V47.5711H221.886V59.1364C221.886 62.695 221.98 66.675 228.488 66.675C229.331 66.675 231.297 66.4877 232.281 65.9258V62.0863C231.438 62.6014 230.267 62.7418 229.284 62.7418C226.1 62.7418 226.1 60.1197 226.1 57.6381V47.5711H232.14V43.9189Z" fill="#25B135"/>
<path d="M252.654 47.0092C251.062 44.3871 248.019 43.357 245.069 43.357C240.902 43.357 236.641 45.5577 236.641 50.1932C236.641 54.4541 239.825 55.6247 243.43 56.5143C245.256 56.9358 248.768 57.404 248.768 59.9324C248.768 61.8522 246.427 62.7418 244.273 62.7418C241.838 62.7418 240.246 61.4776 238.888 60.1197L235.704 62.7418C237.905 65.7385 240.621 66.675 244.273 66.675C248.674 66.675 253.263 64.7084 253.263 59.6983C253.263 55.5311 250.454 54.1264 246.801 53.2367C244.929 52.8153 241.136 52.4407 241.136 49.7718C241.136 48.1798 242.868 47.2902 244.835 47.2902C246.942 47.2902 248.393 48.2735 249.377 49.4909L252.654 47.0092Z" fill="#25B135"/>
</g>
<defs>
<linearGradient id="paint0_linear" x1="34.0826" y1="36.6504" x2="34.0826" y2="77.1139" gradientUnits="userSpaceOnUse">
<stop stop-color="#25B135"/>
<stop offset="1" stop-color="#005209"/>
</linearGradient>
<clipPath id="clip0">
<rect width="280" height="70" fill="white" transform="translate(0 0.671875)"/>
</clipPath>
</defs>
</svg>
</div>
<div class="connection-badge">
<div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
<div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
</div>
</ng-container>
</a>
<div ngbDropdown (window:resize)="onResize($event)" class="dropdown-container" *ngIf="env.TESTNET_ENABLED || env.SIGNET_ENABLED || env.LIQUID_ENABLED || env.BISQ_ENABLED || env.LIQUID_TESTNET_ENABLED">
<button ngbDropdownToggle type="button" class="btn btn-secondary dropdown-toggle-split d-flex justify-content-center align-items-center" aria-haspopup="true">
<app-svg-images class="d-flex justify-content-center align-items-center current-network-svg" name="bisq" width="20" height="20" viewBox="0 0 80 80"></app-svg-images>
</button>
<div ngbDropdownMenu [ngClass]="{'dropdown-menu-right' : isMobile}">
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['mainnet'] || '/')" ngbDropdownItem class="mainnet"><app-svg-images name="bitcoin" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Mainnet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['signet'] || '/signet')" ngbDropdownItem *ngIf="env.SIGNET_ENABLED" class="signet"><app-svg-images name="signet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Signet</a>
<a [href]="env.MEMPOOL_WEBSITE_URL + urlLanguage + (networkPaths['testnet'] || '/testnet')" ngbDropdownItem *ngIf="env.TESTNET_ENABLED" class="testnet"><app-svg-images name="testnet" width="22" height="22" viewBox="0 0 65 65" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Testnet</a>
<h6 class="dropdown-header" i18n="master-page.layer2-networks-header">Layer 2 Networks</h6>
<a ngbDropdownItem class="mainnet active" [routerLink]="networkPaths['bisq'] || '/'"><app-svg-images name="bisq" width="20" height="20" viewBox="0 0 75 75" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Bisq</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquid'] || '/')" ngbDropdownItem *ngIf="env.LIQUID_ENABLED" class="liquid"><app-svg-images name="liquid" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid</a>
<a [href]="env.LIQUID_WEBSITE_URL + urlLanguage + (networkPaths['liquidtestnet'] || '/testnet')" ngbDropdownItem *ngIf="env.LIQUID_TESTNET_ENABLED" class="liquidtestnet"><app-svg-images name="liquidtestnet" width="22" height="22" viewBox="0 0 125 125" style="width: 25px; height: 25px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</a>
</div>
</div>
<div class="navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav">
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-home">
<a class="nav-link" [routerLink]="['/' | relativeUrl]" (click)="collapse()"><fa-icon [icon]="['fas', 'tachometer-alt']" [fixedWidth]="true" i18n-title="master-page.dashboard" title="Dashboard"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" id="btn-transactions">
<a class="nav-link" [routerLink]="['/transactions']" (click)="collapse()"><fa-icon [icon]="['fas', 'list']" [fixedWidth]="true" i18n-title="master-page.transactions" title="Transactions"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-blocks">
<a class="nav-link" [routerLink]="['/blocks']" (click)="collapse()"><fa-icon [icon]="['fas', 'cubes']" [fixedWidth]="true" i18n-title="master-page.blocks" title="Blocks"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-stats">
<a class="nav-link" [routerLink]="['/stats']" (click)="collapse()"><fa-icon [icon]="['fas', 'file-alt']" [fixedWidth]="true" i18n-title="master-page.stats" title="Stats"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-docs">
<a class="nav-link" [routerLink]="['/docs']" (click)="collapse()"><fa-icon [icon]="['fas', 'book']" [fixedWidth]="true" i18n-title="master-page.docs" title="Docs"></fa-icon></a>
</li>
<li class="nav-item" routerLinkActive="active" id="btn-about">
<a class="nav-link" [routerLink]="['/about']" (click)="collapse()"><fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true" i18n-title="master-page.about" title="About"></fa-icon></a>
</li>
</ul>
<app-search-form class="search-form-container" location="top" (searchTriggered)="collapse()"></app-search-form>
</div>
</nav>
</header>
<br />
<main>
<router-outlet></router-outlet>
</main>
<app-global-footer *ngIf="footerVisible"></app-global-footer>
<br>

View File

@@ -1,172 +0,0 @@
.sticky-header {
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
z-index: 100;
}
li.nav-item.active {
background-color: #653b9c;
}
fa-icon {
font-size: 1.66em;
}
.navbar {
z-index: 100;
min-height: 64px;
}
li.nav-item {
margin: auto 5px;
padding-left: 10px;
padding-right: 10px;
}
@media (max-width: 992px) {
footer > .container-fluid {
padding-bottom: 35px;
}
}
@media (min-width: 992px) {
.navbar {
padding: 0rem 2rem;
}
fa-icon {
font-size: 1.2em;
}
.dropdown-container {
margin-right: 16px;
}
li.nav-item {
margin: auto 0px;
padding: 10px;
}
}
.navbar-nav {
background: #212121;
bottom: 0;
box-shadow: 0px 0px 15px 0px #000;
flex-direction: row;
left: 0;
justify-content: center;
position: fixed;
width: 100%;
@media (min-width: 992px) {
background: transparent;
box-shadow: none;
position: relative;
width: auto;
}
a {
font-size: 0.8em;
@media (min-width: 375px) {
font-size: 1em;
}
}
}
.navbar-collapse {
flex-basis: auto;
justify-content: flex-end;
}
@media (min-width: 992px) {
.navbar-collapse {
justify-content: space-between;
}
}
.navbar-brand {
width: 60%;
}
@media (min-width: 576px) {
.navbar-brand {
width: 140px;
}
}
nav {
box-shadow: 0px 0px 15px 0px #000;
}
.connection-badge {
position: absolute;
top: 13px;
left: 0px;
width: 140px;
}
.badge {
margin: 0 auto;
display: table;
}
.mainnet.active {
background-color: #653b9c;
}
.liquid.active {
background-color: #116761;
}
.liquidtestnet.active {
background-color: #494a4a;
}
.testnet.active {
background-color: #1d486f;
}
.signet.active {
background-color: #6f1d5d;
}
.dropdown-divider {
border-top: 1px solid #121420;
}
.dropdown-toggle::after {
vertical-align: 0.1em;
}
.dropdown-item {
display: flex;
align-items: center;
}
@media (min-width: 992px) {
.search-form-container {
width: 100%;
max-width: 500px;
padding-left: 15px;
}
}
.navbar-dark .navbar-nav .nav-link {
color: #f1f1f1;
}
.navbar-brand {
margin-right: 5px;
}
.current-network-svg {
width: 20px;
height: 20px;
margin-right: 5px;
}
:host-context(.rtl-layout) {
.current-network-svg {
width: 20px;
height: 20px;
margin-left: 5px;
margin-right: 0px;
}
}

View File

@@ -1,50 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Env, StateService } from '../../services/state.service';
import { Observable } from 'rxjs';
import { LanguageService } from '../../services/language.service';
import { EnterpriseService } from '../../services/enterprise.service';
import { NavigationService } from '../../services/navigation.service';
@Component({
selector: 'app-bisq-master-page',
templateUrl: './bisq-master-page.component.html',
styleUrls: ['./bisq-master-page.component.scss'],
})
export class BisqMasterPageComponent implements OnInit {
connectionState$: Observable<number>;
navCollapsed = false;
env: Env;
isMobile = window.innerWidth <= 767.98;
urlLanguage: string;
networkPaths: { [network: string]: string };
footerVisible = true;
constructor(
private stateService: StateService,
private languageService: LanguageService,
private enterpriseService: EnterpriseService,
private navigationService: NavigationService,
) { }
ngOnInit() {
this.env = this.stateService.env;
this.connectionState$ = this.stateService.connectionState$;
this.urlLanguage = this.languageService.getLanguageForUrl();
this.navigationService.subnetPaths.subscribe((paths) => {
this.networkPaths = paths;
if (paths.mainnet.indexOf('docs') > -1) {
this.footerVisible = false;
} else {
this.footerVisible = true;
}
});
}
collapse(): void {
this.navCollapsed = !this.navCollapsed;
}
onResize(event: any) {
this.isMobile = window.innerWidth <= 767.98;
}
}

View File

@@ -62,10 +62,10 @@
</div>
</div>
<div [class.chart]="!widget" [class.chart-widget]="widget" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div [class.chart]="!widget" [class.chart-widget]="widget" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, NgZone, OnInit } from '@angular/core';
import { EChartsOption, graphic } from 'echarts';
import { echarts, EChartsOption } from '../../graphs/echarts';
import { Observable, combineLatest, of } from 'rxjs';
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
import { ApiService } from '../../services/api.service';
@@ -55,7 +55,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
private stateService: StateService,
public stateService: StateService,
private router: Router,
private zone: NgZone,
private route: ActivatedRoute,
@@ -209,7 +209,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
prepareChartOptions(data, weightMode) {
this.chartOptions = {
color: this.widget ? ['#6b6b6b', new graphic.LinearGradient(0, 0, 0, 0.65, [
color: this.widget ? ['#6b6b6b', new echarts.graphic.LinearGradient(0, 0, 0, 0.65, [
{ offset: 0, color: '#F4511E' },
{ offset: 0.25, color: '#FB8C00' },
{ offset: 0.5, color: '#FFB300' },
@@ -282,7 +282,7 @@ export class BlockFeeRatesGraphComponent implements OnInit {
legend: (this.widget || data.series.length === 0) ? undefined : {
padding: [10, 75],
data: data.legends,
selected: JSON.parse(this.storageService.getValue('fee_rates_legend')) ?? {
selected: JSON.parse(this.storageService.getValue('fee_rates_legend') || 'null') ?? {
'Min': true,
'10th': true,
'25th': true,

View File

@@ -36,10 +36,10 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@@ -12,6 +12,7 @@ import { MiningService } from '../../services/mining.service';
import { ActivatedRoute } from '@angular/router';
import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe';
import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-block-fees-graph',
@@ -54,6 +55,7 @@ export class BlockFeesGraphComponent implements OnInit {
private formBuilder: UntypedFormBuilder,
private storageService: StorageService,
private miningService: MiningService,
public stateService: StateService,
private route: ActivatedRoute,
private fiatShortenerPipe: FiatShortenerPipe,
private fiatCurrencyPipe: FiatCurrencyPipe,

View File

@@ -45,10 +45,10 @@
</form>
</div>
<div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
<div class="chart" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions"
(chartInit)="onChartInit($event)">
</div>
<div class="text-center loadingGraphs" *ngIf="isLoading">
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser || isLoading">
<div class="spinner-border text-light"></div>
</div>

View File

@@ -52,7 +52,7 @@ export class BlockHealthGraphComponent implements OnInit {
private storageService: StorageService,
private zone: NgZone,
private route: ActivatedRoute,
private stateService: StateService,
public stateService: StateService,
private router: Router,
) {
this.radioGroupForm = this.formBuilder.group({ dateSpan: '1y' });

View File

@@ -1,7 +1,7 @@
<div class="grid-align" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
<div class="graph-alignment" [class.grid-align]="!autofit" [style.gridTemplateColumns]="'repeat(auto-fit, ' + resolution + 'px)'">
<div class="block-overview-graph">
<canvas class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<canvas *browserOnly class="block-overview-canvas" [class.clickable]="!!hoverTx" #blockCanvas></canvas>
<div class="loader-wrapper" [class.hidden]="(!isLoading || disableSpinner) && !unavailable">
<div *ngIf="!unavailable" class="spinner-border ml-3 loading" role="status"></div>
<div *ngIf="!isLoading && unavailable" class="ml-3" i18n="block.not-available">not available</div>
@@ -12,6 +12,9 @@
[clickable]="!!selectedTx"
[auditEnabled]="auditHighlighting"
[blockConversion]="blockConversion"
[filterFlags]="activeFilterFlags"
[filterMode]="filterMode"
[relativeTime]="relativeTime"
></app-block-overview-tooltip>
<app-block-filters *ngIf="webGlEnabled && showFilters && filtersAvailable" [excludeFilters]="excludeFilters" [cssWidth]="cssWidth" (onFilterChanged)="setFilterFlags($event)"></app-block-filters>
<div *ngIf="!webGlEnabled" class="placeholder">

View File

@@ -22,9 +22,12 @@
}
}
.grid-align {
.graph-alignment {
position: relative;
width: 100%;
}
.grid-align {
display: grid;
grid-template-columns: repeat(auto-fit, 75px);
justify-content: center;

View File

@@ -1,5 +1,5 @@
import { Component, ElementRef, ViewChild, HostListener, Input, Output, EventEmitter, NgZone, AfterViewInit, OnDestroy, OnChanges } from '@angular/core';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { FastVertexArray } from './fast-vertex-array';
import BlockScene from './block-scene';
import TxSprite from './tx-sprite';
@@ -20,7 +20,7 @@ const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
added: setOpacity(defaultAuditColors.added, unmatchedOpacity),
selected: setOpacity(defaultAuditColors.selected, unmatchedOpacity),
prioritized: setOpacity(defaultAuditColors.prioritized, unmatchedOpacity),
accelerated: setOpacity(defaultAuditColors.accelerated, unmatchedOpacity),
};
@@ -32,6 +32,7 @@ const unmatchedAuditColors = {
export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, OnChanges {
@Input() isLoading: boolean;
@Input() resolution: number;
@Input() autofit: boolean = false;
@Input() blockLimit: number;
@Input() orientation = 'left';
@Input() flip = true;
@@ -45,6 +46,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and';
@Input() relativeTime: number | null;
@Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null;
@Output() txClickEvent = new EventEmitter<{ tx: TransactionStripped, keyModifier: boolean}>();
@@ -83,9 +85,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
constructor(
readonly ngZone: NgZone,
readonly elRef: ElementRef,
private stateService: StateService,
public stateService: StateService,
) {
this.webGlEnabled = detectWebGL();
this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
this.vertexArray = new FastVertexArray(512, TxSprite.dataSize);
this.searchSubscription = this.stateService.searchText$.subscribe((text) => {
this.searchText = text;
@@ -94,13 +96,15 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
ngAfterViewInit(): void {
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.canvas) {
this.canvas.nativeElement.addEventListener('webglcontextlost', this.handleContextLost, false);
this.canvas.nativeElement.addEventListener('webglcontextrestored', this.handleContextRestored, false);
this.gl = this.canvas.nativeElement.getContext('webgl');
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
}
}
}
@@ -142,8 +146,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
cancelAnimationFrame(this.animationFrameRequest);
clearTimeout(this.animationHeartBeat);
}
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
if (this.canvas) {
this.canvas.nativeElement.removeEventListener('webglcontextlost', this.handleContextLost);
this.canvas.nativeElement.removeEventListener('webglcontextrestored', this.handleContextRestored);
}
}
clear(direction): void {
@@ -202,6 +208,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void {
if (this.scene) {
add = add.filter(tx => !this.scene.txs[tx.txid]);
remove = remove.filter(txid => this.scene.txs[txid]);
change = change.filter(tx => this.scene.txs[tx.txid]);
this.scene.update(add, remove, change, direction, resetLayout);
this.start();
this.updateSearchHighlight();
@@ -209,6 +219,10 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
initCanvas(): void {
if (!this.canvas || !this.gl) {
return;
}
this.gl.clearColor(0.0, 0.0, 0.0, 0.0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
@@ -262,24 +276,26 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@HostListener('window:resize', ['$event'])
resizeCanvas(): void {
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
this.displayWidth = window.devicePixelRatio * this.cssWidth;
this.displayHeight = window.devicePixelRatio * this.cssHeight;
this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
}
if (this.scene) {
this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
this.start();
} else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
if (this.canvas) {
this.cssWidth = this.canvas.nativeElement.offsetParent.clientWidth;
this.cssHeight = this.canvas.nativeElement.offsetParent.clientHeight;
this.displayWidth = window.devicePixelRatio * this.cssWidth;
this.displayHeight = window.devicePixelRatio * this.cssHeight;
this.canvas.nativeElement.width = this.displayWidth;
this.canvas.nativeElement.height = this.displayHeight;
if (this.gl) {
this.gl.viewport(0, 0, this.displayWidth, this.displayHeight);
}
if (this.scene) {
this.scene.resize({ width: this.displayWidth, height: this.displayHeight, animate: false });
this.start();
} else {
this.scene = new BlockScene({ width: this.displayWidth, height: this.displayHeight, resolution: this.resolution,
blockLimit: this.blockLimit, orientation: this.orientation, flip: this.flip, vertexArray: this.vertexArray,
highlighting: this.auditHighlighting, animationDuration: this.animationDuration, animationOffset: this.animationOffset,
colorFunction: this.getColorFunction() });
this.start();
this.start();
}
}
}
@@ -406,6 +422,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@HostListener('pointerup', ['$event'])
onClick(event) {
if (!this.canvas) {
return;
}
if (event.target === this.canvas.nativeElement && event.pointerType === 'touch') {
this.setPreviewTx(event.offsetX, event.offsetY, true);
} else if (event.target === this.canvas.nativeElement) {
@@ -417,6 +436,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@HostListener('pointermove', ['$event'])
onPointerMove(event) {
if (!this.canvas) {
return;
}
if (event.target === this.canvas.nativeElement) {
this.setPreviewTx(event.offsetX, event.offsetY, false);
} else {

View File

@@ -1,6 +1,6 @@
import { FastVertexArray } from './fast-vertex-array';
import TxView from './tx-view';
import { TransactionStripped } from '../../interfaces/websocket.interface';
import { TransactionStripped } from '../../interfaces/node-api.interface';
import { Color, Position, Square, ViewUpdateParams } from './sprite-types';
import { defaultColorFunction } from './utils';

Some files were not shown because too many files have changed in this diff Show More