Compare commits

...

23 Commits

Author SHA1 Message Date
softsimon
3a8c46bbed Merge pull request #4837 from mempool/mononaut/goggles-age-filter
Goggles age filter
2024-04-04 16:47:34 +09:00
Mononaut
fc5312549d Switch gradient toggle fee -> default 2024-04-04 07:42:31 +00:00
softsimon
c14e8797e2 Merge pull request #4879 from mempool/nymkappa/proxy-accel-endpoints
[accelerator] proxy acceleration api to prod
2024-04-04 16:20:27 +09:00
nymkappa
0f26940018 Merge branch 'master' into nymkappa/proxy-accel-endpoints 2024-04-04 16:01:17 +09:00
Mononaut
26227e2f3b New opacity-based age Goggles 2024-04-04 06:57:38 +00:00
Mononaut
dcf78fab06 Block visualization color-by-age mode 2024-04-04 06:57:37 +00:00
softsimon
0813592a6d Merge pull request #4882 from mempool/nymkappa/reset-pool-sha
[migration] reset mining pool sha to force refreshing
2024-04-04 15:20:04 +09:00
nymkappa
abdb27af3f [migration] reset mining pool sha to force refreshing 2024-04-04 14:27:50 +09:00
nymkappa
b421be3315 [accelerator] also forward headers 2024-04-04 14:04:12 +09:00
softsimon
b74fbee069 Merge pull request #4881 from mempool/nymkappa/proxy-sponsors
[about page] proxy community sponsors apis to prod, small refactor
2024-04-04 14:02:00 +09:00
nymkappa
dab9357b40 [about page] proxy community sponsors apis to prod, small refactor 2024-04-04 13:56:39 +09:00
softsimon
ccd89604c0 Merge pull request #4856 from mempool/hunicus/docs-links-alignment
Fix api-docs anchor link vertical alignment
2024-04-04 13:54:53 +09:00
Felipe Knorr Kuhn
cd135b7171 Merge branch 'master' into nymkappa/proxy-accel-endpoints 2024-04-04 13:14:54 +09:00
softsimon
7e16d550b0 Merge pull request #4880 from mempool/knorrium/enable_fiat_price_again
Enable fiat price conversion again
2024-04-04 13:14:24 +09:00
nymkappa
404079ef4e [accelerator] use config.MEMPOOL_SERVICES.API url 2024-04-04 13:10:32 +09:00
Felipe Knorr Kuhn
d13f78f046 Enable fiat price conversion again 2024-04-04 13:10:11 +09:00
nymkappa
60040c3914 [accelerator] proxy acceleration api to prod 2024-04-04 12:57:54 +09:00
softsimon
e408fbd8d6 Merge branch 'master' into hunicus/docs-links-alignment 2024-04-04 12:40:14 +09:00
softsimon
6931ecd468 Merge pull request #4878 from mempool/dependabot/npm_and_yarn/backend/multi-b37afcb6a9
Bump ws and @types/ws in /backend
2024-04-04 12:31:27 +09:00
dependabot[bot]
ed5d30ea5b Bump ws and @types/ws in /backend
Bumps [ws](https://github.com/websockets/ws) and [@types/ws](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/ws). These dependencies needed to be updated together.

Updates `ws` from 8.13.0 to 8.16.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.13.0...8.16.0)

Updates `@types/ws` from 8.5.5 to 8.5.10
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/ws)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/ws"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 02:49:18 +00:00
softsimon
55f9d0f875 Merge branch 'master' into hunicus/docs-links-alignment 2024-04-03 18:57:46 +09:00
hunicus
fb6aec0afe Merge branch 'master' into hunicus/docs-links-alignment 2024-04-01 19:06:18 +09:00
hunicus
b8a48314c1 Fix api-docs anchor link vertical alignment 2024-04-01 18:53:38 +09:00
26 changed files with 355 additions and 152 deletions

View File

@@ -23,7 +23,7 @@
"rust-gbt": "file:./rust-gbt", "rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3", "typescript": "~4.9.3",
"ws": "~8.13.0" "ws": "~8.16.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
@@ -32,7 +32,7 @@
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/ws": "~8.5.5", "@types/ws": "~8.5.10",
"@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0", "@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0", "eslint": "^8.36.0",
@@ -1858,9 +1858,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.5.5", "version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
@@ -7690,9 +7690,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.13.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@@ -9198,9 +9198,9 @@
"dev": true "dev": true
}, },
"@types/ws": { "@types/ws": {
"version": "8.5.5", "version": "8.5.10",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz",
"integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/node": "*" "@types/node": "*"
@@ -13424,9 +13424,9 @@
} }
}, },
"ws": { "ws": {
"version": "8.13.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"requires": {} "requires": {}
}, },
"y18n": { "y18n": {

View File

@@ -52,7 +52,7 @@
"redis": "^4.6.6", "redis": "^4.6.6",
"socks-proxy-agent": "~7.0.0", "socks-proxy-agent": "~7.0.0",
"typescript": "~4.9.3", "typescript": "~4.9.3",
"ws": "~8.13.0" "ws": "~8.16.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.18.6",
@@ -61,7 +61,7 @@
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/ws": "~8.5.5", "@types/ws": "~8.5.10",
"@typescript-eslint/eslint-plugin": "^5.55.0", "@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0", "@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0", "eslint": "^8.36.0",

View File

@@ -0,0 +1,87 @@
import { Application } from "express";
import config from "../config";
import axios from "axios";
import logger from "../logger";
class AboutRoutes {
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'services/sponsors', async (req, res) => {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to fetch sponsors from ${url}. ${e}`, 'About Page');
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'services/account/images/:username', async (req, res) => {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to fetch sponsor profile image from ${url}. ${e}`, 'About Page');
res.status(500).end();
}
})
;
}
}
export default new AboutRoutes();

View File

@@ -0,0 +1,75 @@
import { Application, Request, Response } from "express";
import config from "../../config";
import axios from "axios";
import logger from "../../logger";
class AccelerationRoutes {
private tag = 'Accelerator';
public initRoutes(app: Application) {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations', this.$getAcceleratorAccelerations.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history', this.$getAcceleratorAccelerationsHistory.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/history/aggregated', this.$getAcceleratorAccelerationsHistoryAggregated.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'services/accelerator/accelerations/stats', this.$getAcceleratorAccelerationsStats.bind(this))
;
}
private async $getAcceleratorAccelerations(req: Request, res: Response) {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get current accelerations from ${url} in $getAcceleratorAccelerations(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorAccelerationsHistory(req: Request, res: Response) {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration history from ${url} in $getAcceleratorAccelerationsHistory(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorAccelerationsHistoryAggregated(req: Request, res: Response) {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get aggregated acceleration history from ${url} in $getAcceleratorAccelerationsHistoryAggregated(), ${e}`, this.tag);
res.status(500).end();
}
}
private async $getAcceleratorAccelerationsStats(req: Request, res: Response) {
const url = `${config.MEMPOOL_SERVICES.API}/${req.originalUrl.replace('/api/v1/services/', '')}`;
try {
const response = await axios.get(url, { responseType: 'stream', timeout: 10000 });
for (const key in response.headers) {
res.setHeader(key, response.headers[key]);
}
response.data.pipe(res);
} catch (e) {
logger.err(`Unable to get acceleration stats from ${url} in $getAcceleratorAccelerationsStats(), ${e}`, this.tag);
res.status(500).end();
}
}
}
export default new AccelerationRoutes();

View File

@@ -1,6 +1,6 @@
import logger from '../logger'; import logger from '../../logger';
import { MempoolTransactionExtended } from '../mempool.interfaces'; import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { IEsploraApi } from './bitcoin/esplora-api.interface'; import { IEsploraApi } from '../bitcoin/esplora-api.interface';
const BLOCK_WEIGHT_UNITS = 4_000_000; const BLOCK_WEIGHT_UNITS = 4_000_000;
const BLOCK_SIGOPS = 80_000; const BLOCK_SIGOPS = 80_000;

View File

@@ -37,60 +37,6 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements) .get(config.MEMPOOL.API_URL_PREFIX + 'replacements', this.getRbfReplacements)
.get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements) .get(config.MEMPOOL.API_URL_PREFIX + 'fullrbf/replacements', this.getFullRbfReplacements)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm) .post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
.get(config.MEMPOOL.API_URL_PREFIX + 'donations', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'donations/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/donations/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'contributors/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/contributors/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators`, { responseType: 'stream', timeout: 10000 });
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'translators/images/:id', async (req, res) => {
try {
const response = await axios.get(`${config.EXTERNAL_DATA_SERVER.MEMPOOL_API}/translators/images/${req.params.id}`, {
responseType: 'stream', timeout: 10000
});
response.data.pipe(res);
} catch (e) {
res.status(500).end();
}
})
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/:height', this.getBlocks.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', this.getBlock)

View File

@@ -655,6 +655,7 @@ class DatabaseMigration {
await this.$executeQuery('TRUNCATE hashrates'); await this.$executeQuery('TRUNCATE hashrates');
await this.$executeQuery('TRUNCATE difficulty_adjustments'); await this.$executeQuery('TRUNCATE difficulty_adjustments');
await this.$executeQuery(`UPDATE state SET string = NULL WHERE name = 'pools_json_sha'`);
await this.updateToSchemaVersion(75); await this.updateToSchemaVersion(75);
} }

View File

@@ -43,6 +43,8 @@ import redisCache from './api/redis-cache';
import accelerationApi from './api/services/acceleration'; import accelerationApi from './api/services/acceleration';
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes'; import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client'; import bitcoinSecondClient from './api/bitcoin/bitcoin-second-client';
import accelerationRoutes from './api/acceleration/acceleration.routes';
import aboutRoutes from './api/about.routes';
class Server { class Server {
private wss: WebSocket.Server | undefined; private wss: WebSocket.Server | undefined;
@@ -305,6 +307,10 @@ class Server {
nodesRoutes.initRoutes(this.app); nodesRoutes.initRoutes(this.app);
channelsRoutes.initRoutes(this.app); channelsRoutes.initRoutes(this.app);
} }
if (config.MEMPOOL_SERVICES.ACCELERATIONS) {
accelerationRoutes.initRoutes(this.app);
}
aboutRoutes.initRoutes(this.app);
} }
healthCheck(): void { healthCheck(): void {

View File

@@ -1,4 +1,4 @@
import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration'; import { AccelerationInfo, makeBlockTemplate } from '../api/acceleration/acceleration';
import { RowDataPacket } from 'mysql2'; import { RowDataPacket } from 'mysql2';
import DB from '../database'; import DB from '../database';
import logger from '../logger'; import logger from '../logger';
@@ -7,7 +7,7 @@ import { Common } from '../api/common';
import config from '../config'; import config from '../config';
import blocks from '../api/blocks'; import blocks from '../api/blocks';
import accelerationApi, { Acceleration } from '../api/services/acceleration'; import accelerationApi, { Acceleration } from '../api/services/acceleration';
import accelerationCosts from '../api/acceleration'; import accelerationCosts from '../api/acceleration/acceleration';
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory'; import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
import transactionUtils from '../api/transaction-utils'; import transactionUtils from '../api/transaction-utils';
import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces'; import { BlockExtended, MempoolTransactionExtended } from '../mempool.interfaces';

View File

@@ -149,7 +149,7 @@ __REDIS_UNIX_SOCKET_PATH__=${REDIS_UNIX_SOCKET_PATH:=true}
__REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000} __REDIS_BATCH_QUERY_BASE_SIZE__=${REDIS_BATCH_QUERY_BASE_SIZE:=5000}
# FIAT_PRICE # FIAT_PRICE
__FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=false} __FIAT_PRICE_ENABLED__=${FIAT_PRICE_ENABLED:=true}
__FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""} __FIAT_PRICE_API_KEY__=${FIAT_PRICE_API_KEY:=""}
mkdir -p "${__MEMPOOL_CACHE_DIR__}" mkdir -p "${__MEMPOOL_CACHE_DIR__}"

View File

@@ -263,7 +263,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
imports: [RouterModule.forRoot(routes, { imports: [RouterModule.forRoot(routes, {
initialNavigation: 'enabledBlocking', initialNavigation: 'enabledBlocking',
scrollPositionRestoration: 'enabled', scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled', anchorScrolling: 'disabled',
preloadingStrategy: AppPreloadingStrategy preloadingStrategy: AppPreloadingStrategy
})], })],
}) })

View File

@@ -14,14 +14,29 @@
</div> </div>
</div> </div>
<div class="filter-menu" *ngIf="menuOpen && cssWidth > 280"> <div class="filter-menu" *ngIf="menuOpen && cssWidth > 280">
<h5>Match</h5> <div class="filter-row">
<div class="btn-group btn-group-toggle"> <div class="filter-element">
<label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'"> <h5>Match</h5>
<input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All <div class="btn-group btn-group-toggle">
</label> <label class="btn btn-xs blue mode-toggle" [class.active]="filterMode === 'and'">
<label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'"> <input type="radio" [value]="'all'" fragment="all" (click)="setFilterMode('and')">All
<input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any </label>
</label> <label class="btn btn-xs green mode-toggle" [class.active]="filterMode === 'or'">
<input type="radio" [value]="'any'" fragment="any" (click)="setFilterMode('or')">Any
</label>
</div>
</div>
<div class="filter-element">
<h5>Gradient</h5>
<div class="btn-group btn-group-toggle">
<label class="btn btn-xs yellow mode-toggle" [class.active]="gradientMode === 'fee'">
<input type="radio" [value]="'fee'" fragment="default" (click)="setGradientMode('fee')">Default
</label>
<label class="btn btn-xs blue mode-toggle" [class.active]="gradientMode === 'age'">
<input type="radio" [value]="'age'" fragment="age" (click)="setGradientMode('age')">Age
</label>
</div>
</div>
</div> </div>
<ng-container *ngFor="let group of filterGroups;"> <ng-container *ngFor="let group of filterGroups;">
<h5>{{ group.label }}</h5> <h5>{{ group.label }}</h5>

View File

@@ -45,6 +45,13 @@
} }
.filter-menu { .filter-menu {
.filter-row {
display: flex;
flex-direction: row;
justify-content: start;
align-items: baseline;
}
h5 { h5 {
font-size: 0.8rem; font-size: 0.8rem;
color: white; color: white;
@@ -118,6 +125,12 @@
background: #1a9436; background: #1a9436;
} }
} }
&.yellow {
border: solid 1px #bf7815;
&.active {
background: #bf7815;
}
}
} }
:host-context(.block-overview-graph:hover) &, &:hover, &:active { :host-context(.block-overview-graph:hover) &, &:hover, &:active {

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core'; import { Component, EventEmitter, Output, HostListener, Input, ChangeDetectorRef, OnChanges, SimpleChanges, OnInit, OnDestroy } from '@angular/core';
import { ActiveFilter, FilterGroups, FilterMode, TransactionFilters } from '../../shared/filters.utils'; import { ActiveFilter, FilterGroups, FilterMode, GradientMode, TransactionFilters } from '../../shared/filters.utils';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@@ -22,6 +22,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
activeFilters: string[] = []; activeFilters: string[] = [];
filterFlags: { [key: string]: boolean } = {}; filterFlags: { [key: string]: boolean } = {};
filterMode: FilterMode = 'and'; filterMode: FilterMode = 'and';
gradientMode: GradientMode = 'fee';
menuOpen: boolean = false; menuOpen: boolean = false;
constructor( constructor(
@@ -32,6 +33,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => { this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
this.filterMode = active.mode; this.filterMode = active.mode;
this.gradientMode = active.gradient;
for (const key of Object.keys(this.filterFlags)) { for (const key of Object.keys(this.filterFlags)) {
this.filterFlags[key] = false; this.filterFlags[key] = false;
} }
@@ -39,7 +41,7 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
this.filterFlags[key] = !this.disabledFilters[key]; this.filterFlags[key] = !this.disabledFilters[key];
} }
this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])]; this.activeFilters = [...active.filters.filter(key => !this.disabledFilters[key])];
this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters }); this.onFilterChanged.emit({ mode: active.mode, filters: this.activeFilters, gradient: this.gradientMode });
}); });
} }
@@ -57,8 +59,14 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
setFilterMode(mode): void { setFilterMode(mode): void {
this.filterMode = mode; this.filterMode = mode;
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters }); this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] }); this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode });
}
setGradientMode(mode): void {
this.gradientMode = mode;
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode });
} }
toggleFilter(key): void { toggleFilter(key): void {
@@ -81,8 +89,8 @@ export class BlockFiltersComponent implements OnInit, OnChanges, OnDestroy {
this.activeFilters = this.activeFilters.filter(f => f != key); this.activeFilters = this.activeFilters.filter(f => f != key);
} }
const booleanFlags = this.getBooleanFlags(); const booleanFlags = this.getBooleanFlags();
this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters }); this.onFilterChanged.emit({ mode: this.filterMode, filters: this.activeFilters, gradient: this.gradientMode });
this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters] }); this.stateService.activeGoggles$.next({ mode: this.filterMode, filters: [...this.activeFilters], gradient: this.gradientMode });
} }
getBooleanFlags(): bigint | null { getBooleanFlags(): bigint | null {

View File

@@ -8,14 +8,11 @@ import { Color, Position } from './sprite-types';
import { Price } from '../../services/price.service'; import { Price } from '../../services/price.service';
import { StateService } from '../../services/state.service'; import { StateService } from '../../services/state.service';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { defaultColorFunction, setOpacity, defaultFeeColors, defaultAuditFeeColors, defaultMarginalFeeColors, defaultAuditColors } from './utils'; import { defaultColorFunction, setOpacity, defaultAuditColors, defaultColors, ageColorFunction } from './utils';
import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils'; import { ActiveFilter, FilterMode, toFlags } from '../../shared/filters.utils';
import { detectWebGL } from '../../shared/graphs.utils'; import { detectWebGL } from '../../shared/graphs.utils';
const unmatchedOpacity = 0.2; const unmatchedOpacity = 0.2;
const unmatchedFeeColors = defaultFeeColors.map(c => setOpacity(c, unmatchedOpacity));
const unmatchedAuditFeeColors = defaultAuditFeeColors.map(c => setOpacity(c, unmatchedOpacity));
const unmatchedMarginalFeeColors = defaultMarginalFeeColors.map(c => setOpacity(c, unmatchedOpacity));
const unmatchedAuditColors = { const unmatchedAuditColors = {
censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity), censored: setOpacity(defaultAuditColors.censored, unmatchedOpacity),
missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity), missing: setOpacity(defaultAuditColors.missing, unmatchedOpacity),
@@ -46,6 +43,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@Input() excludeFilters: string[] = []; @Input() excludeFilters: string[] = [];
@Input() filterFlags: bigint | null = null; @Input() filterFlags: bigint | null = null;
@Input() filterMode: FilterMode = 'and'; @Input() filterMode: FilterMode = 'and';
@Input() gradientMode: 'fee' | 'age' = 'fee';
@Input() relativeTime: number | null; @Input() relativeTime: number | null;
@Input() blockConversion: Price; @Input() blockConversion: Price;
@Input() overrideColors: ((tx: TxView) => Color) | null = null; @Input() overrideColors: ((tx: TxView) => Color) | null = null;
@@ -121,21 +119,22 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
this.setHighlightingEnabled(this.auditHighlighting); this.setHighlightingEnabled(this.auditHighlighting);
} }
if (changes.overrideColor && this.scene) { if (changes.overrideColor && this.scene) {
this.scene.setColorFunction(this.overrideColors); this.scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
} }
if ((changes.filterFlags || changes.showFilters || changes.filterMode)) { if ((changes.filterFlags || changes.showFilters || changes.filterMode || changes.gradientMode)) {
this.setFilterFlags(); this.setFilterFlags();
} }
} }
setFilterFlags(goggle?: ActiveFilter): void { setFilterFlags(goggle?: ActiveFilter): void {
this.filterMode = goggle?.mode || this.filterMode; this.filterMode = goggle?.mode || this.filterMode;
this.gradientMode = goggle?.gradient || this.gradientMode;
this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags; this.activeFilterFlags = goggle?.filters ? toFlags(goggle.filters) : this.filterFlags;
if (this.scene) { if (this.scene) {
if (this.activeFilterFlags != null && this.filtersAvailable) { if (this.activeFilterFlags != null && this.filtersAvailable) {
this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags)); this.scene.setColorFunction(this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode));
} else { } else {
this.scene.setColorFunction(this.overrideColors); this.scene.setColorFunction(this.getFilterColorFunction(0n, this.gradientMode));
} }
} }
this.start(); this.start();
@@ -212,6 +211,9 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
remove = remove.filter(txid => this.scene.txs[txid]); remove = remove.filter(txid => this.scene.txs[txid]);
change = change.filter(tx => this.scene.txs[tx.txid]); change = change.filter(tx => this.scene.txs[tx.txid]);
if (this.gradientMode === 'age') {
this.scene.updateAllColors();
}
this.scene.update(add, remove, change, direction, resetLayout); this.scene.update(add, remove, change, direction, resetLayout);
this.start(); this.start();
this.updateSearchHighlight(); this.updateSearchHighlight();
@@ -548,25 +550,24 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
getColorFunction(): ((tx: TxView) => Color) { getColorFunction(): ((tx: TxView) => Color) {
if (this.filterFlags) { if (this.filterFlags) {
return this.getFilterColorFunction(this.filterFlags); return this.getFilterColorFunction(this.filterFlags, this.gradientMode);
} else if (this.activeFilterFlags) { } else if (this.activeFilterFlags) {
return this.getFilterColorFunction(this.activeFilterFlags); return this.getFilterColorFunction(this.activeFilterFlags, this.gradientMode);
} else { } else {
return this.overrideColors; return this.getFilterColorFunction(0n, this.gradientMode);
} }
} }
getFilterColorFunction(flags: bigint): ((tx: TxView) => Color) { getFilterColorFunction(flags: bigint, gradient: 'fee' | 'age'): ((tx: TxView) => Color) {
return (tx: TxView) => { return (tx: TxView) => {
if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) { if ((this.filterMode === 'and' && (tx.bigintFlags & flags) === flags) || (this.filterMode === 'or' && (flags === 0n || (tx.bigintFlags & flags) > 0n))) {
return defaultColorFunction(tx); return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
} else { } else {
return defaultColorFunction( return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
tx, tx,
unmatchedFeeColors, defaultColors.unmatchedfee,
unmatchedAuditFeeColors, unmatchedAuditColors,
unmatchedMarginalFeeColors, this.relativeTime || (Date.now() / 1000)
unmatchedAuditColors
); );
} }
}; };

View File

@@ -68,6 +68,10 @@ export default class BlockScene {
setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void { setColorFunction(colorFunction: ((tx: TxView) => Color) | null): void {
this.getColor = colorFunction || defaultColorFunction; this.getColor = colorFunction || defaultColorFunction;
this.updateAllColors();
}
updateAllColors(): void {
this.dirty = true; this.dirty = true;
if (this.initialised && this.scene) { if (this.initialised && this.scene) {
this.updateColors(performance.now(), 50); this.updateColors(performance.now(), 50);

View File

@@ -37,10 +37,36 @@ export function setOpacity(color: Color, opacity: number): Color {
}; };
} }
interface ColorPalette {
base: Color[],
audit: Color[],
marginal: Color[],
baseLevel: (tx: TxView, rate: number, time: number) => number,
}
// precomputed colors // precomputed colors
export const defaultFeeColors = mempoolFeeColors.map(hexToColor); const defaultColors: { [key: string]: ColorPalette } = {
export const defaultAuditFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.3), 0.9)); fee: {
export const defaultMarginalFeeColors = defaultFeeColors.map((color) => darken(desaturate(color, 0.8), 1.1)); base: mempoolFeeColors.map(hexToColor),
audit: [],
marginal: [],
baseLevel: (tx: TxView, rate: number) => feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1
},
}
for (const key in defaultColors) {
const base = defaultColors[key].base;
defaultColors[key].audit = base.map((color) => darken(desaturate(color, 0.3), 0.9));
defaultColors[key].marginal = base.map((color) => darken(desaturate(color, 0.8), 1.1));
defaultColors['unmatched' + key] = {
base: defaultColors[key].base.map(c => setOpacity(c, 0.2)),
audit: defaultColors[key].audit.map(c => setOpacity(c, 0.2)),
marginal: defaultColors[key].marginal.map(c => setOpacity(c, 0.2)),
baseLevel: defaultColors[key].baseLevel,
};
}
export { defaultColors as defaultColors };
export const defaultAuditColors = { export const defaultAuditColors = {
censored: hexToColor('f344df'), censored: hexToColor('f344df'),
missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7), missing: darken(desaturate(hexToColor('f344df'), 0.3), 0.7),
@@ -51,22 +77,21 @@ export const defaultAuditColors = {
export function defaultColorFunction( export function defaultColorFunction(
tx: TxView, tx: TxView,
feeColors: Color[] = defaultFeeColors, colors: { base: Color[], audit: Color[], marginal: Color[], baseLevel: (tx: TxView, rate: number, time: number) => number } = defaultColors.fee,
auditFeeColors: Color[] = defaultAuditFeeColors, auditColors: { [status: string]: Color } = defaultAuditColors,
marginalFeeColors: Color[] = defaultMarginalFeeColors, relativeTime?: number,
auditColors: { [status: string]: Color } = defaultAuditColors
): Color { ): Color {
const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate const rate = tx.fee / tx.vsize; // color by simple single-tx fee rate
const feeLevelIndex = feeLevels.findIndex((feeLvl) => Math.max(1, rate) < feeLvl) - 1; const levelIndex = colors.baseLevel(tx, rate, relativeTime || (Date.now() / 1000));
const feeLevelColor = feeColors[feeLevelIndex] || feeColors[mempoolFeeColors.length - 1]; const levelColor = colors.base[levelIndex] || colors.base[mempoolFeeColors.length - 1];
// Normal mode // Normal mode
if (!tx.scene?.highlightingEnabled) { if (!tx.scene?.highlightingEnabled) {
if (tx.acc) { if (tx.acc) {
return auditColors.accelerated; return auditColors.accelerated;
} else { } else {
return feeLevelColor; return levelColor;
} }
return feeLevelColor; return levelColor;
} }
// Block audit // Block audit
switch(tx.status) { switch(tx.status) {
@@ -75,7 +100,7 @@ export function defaultColorFunction(
case 'missing': case 'missing':
case 'sigop': case 'sigop':
case 'rbf': case 'rbf':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; return colors.marginal[levelIndex] || colors.marginal[mempoolFeeColors.length - 1];
case 'fresh': case 'fresh':
case 'freshcpfp': case 'freshcpfp':
return auditColors.missing; return auditColors.missing;
@@ -84,20 +109,37 @@ export function defaultColorFunction(
case 'prioritized': case 'prioritized':
return auditColors.prioritized; return auditColors.prioritized;
case 'selected': case 'selected':
return marginalFeeColors[feeLevelIndex] || marginalFeeColors[mempoolFeeColors.length - 1]; return colors.marginal[levelIndex] || colors.marginal[mempoolFeeColors.length - 1];
case 'accelerated': case 'accelerated':
return auditColors.accelerated; return auditColors.accelerated;
case 'found': case 'found':
if (tx.context === 'projected') { if (tx.context === 'projected') {
return auditFeeColors[feeLevelIndex] || auditFeeColors[mempoolFeeColors.length - 1]; return colors.audit[levelIndex] || colors.audit[mempoolFeeColors.length - 1];
} else { } else {
return feeLevelColor; return levelColor;
} }
default: default:
if (tx.acc) { if (tx.acc) {
return auditColors.accelerated; return auditColors.accelerated;
} else { } else {
return feeLevelColor; return levelColor;
} }
} }
}
export function ageColorFunction(
tx: TxView,
colors: { base: Color[], audit: Color[], marginal: Color[], baseLevel: (tx: TxView, rate: number, time: number) => number } = defaultColors.fee,
auditColors: { [status: string]: Color } = defaultAuditColors,
relativeTime?: number,
): Color {
const color = defaultColorFunction(tx, colors, auditColors, relativeTime);
const ageLevel = (!tx.time ? 0 : (0.8 * Math.tanh((1 / 15) * Math.log2((Math.max(1, 0.6 * ((relativeTime - tx.time) - 60)))))));
return {
r: color.r,
g: color.g,
b: color.b,
a: color.a * (1 - ageLevel)
};
} }

View File

@@ -7,7 +7,7 @@ import { ApiService } from '../services/api.service';
import { StateService } from '../services/state.service'; import { StateService } from '../services/state.service';
import { WebsocketService } from '../services/websocket.service'; import { WebsocketService } from '../services/websocket.service';
import { SeoService } from '../services/seo.service'; import { SeoService } from '../services/seo.service';
import { ActiveFilter, FilterMode, toFlags } from '../shared/filters.utils'; import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../shared/filters.utils';
import { detectWebGL } from '../shared/graphs.utils'; import { detectWebGL } from '../shared/graphs.utils';
interface MempoolBlocksData { interface MempoolBlocksData {
@@ -74,14 +74,15 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
private lastReservesBlockUpdate: number = 0; private lastReservesBlockUpdate: number = 0;
goggleResolution = 82; goggleResolution = 82;
goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[] }[] = [ goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [
{ index: 0, name: 'All', mode: 'and', filters: [] }, { index: 0, name: 'All', mode: 'and', filters: [], gradient: 'fee' },
{ index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'] }, { index: 1, name: 'Consolidation', mode: 'and', filters: ['consolidation'], gradient: 'fee' },
{ index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'] }, { index: 2, name: 'Coinjoin', mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
{ index: 3, name: 'Data', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'] }, { index: 3, name: 'Data', mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
]; ];
goggleFlags = 0n; goggleFlags = 0n;
goggleMode: FilterMode = 'and'; goggleMode: FilterMode = 'and';
gradientMode: GradientMode = 'fee';
goggleIndex = 0; goggleIndex = 0;
private destroy$ = new Subject(); private destroy$ = new Subject();
@@ -131,6 +132,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
this.goggleIndex = goggle.index; this.goggleIndex = goggle.index;
this.goggleFlags = toFlags(goggle.filters); this.goggleFlags = toFlags(goggle.filters);
this.goggleMode = goggle.mode; this.goggleMode = goggle.mode;
this.gradientMode = goggle.gradient;
return; return;
} }
} }
@@ -140,6 +142,7 @@ export class DashboardComponent implements OnInit, OnDestroy, AfterViewInit {
name: 'Custom', name: 'Custom',
mode: active.mode, mode: active.mode,
filters: active.filters, filters: active.filters,
gradient: active.gradient,
}); });
this.goggleIndex = this.goggleCycle.length - 1; this.goggleIndex = this.goggleCycle.length - 1;
this.goggleFlags = toFlags(active.filters); this.goggleFlags = toFlags(active.filters);

View File

@@ -4,5 +4,5 @@
</div> </div>
<div *ngFor="let item of tabData"> <div *ngFor="let item of tabData">
<p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ))">{{ item.title }}</p> <p *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ))">{{ item.title }}</p>
<a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" [routerLink]="['./']" fragment="{{ item.fragment }}" (click)="navLinkClick($event)">{{ item.title }}</a> <a *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 ) && ( !item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled ) )" (click)="navLinkClick($event, item.fragment)">{{ item.title }}</a>
</div> </div>

View File

@@ -10,6 +10,7 @@ a {
text-decoration: none; text-decoration: none;
display: block; display: block;
margin: 5px 0; margin: 5px 0;
cursor: pointer;
} }
#enterprise-cta-desktop { #enterprise-cta-desktop {

View File

@@ -33,8 +33,9 @@ export class ApiDocsNavComponent implements OnInit {
} }
} }
navLinkClick(event) { navLinkClick(event, fragment) {
this.navLinkClickEvent.emit(event); event.preventDefault();
this.navLinkClickEvent.emit({event: event, fragment: fragment});
} }
} }

View File

@@ -19,7 +19,7 @@
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled )"> <div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance ) || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('auditOnly') && item.options.auditOnly && auditEnabled )">
<h3 *ngIf="item.type === 'category'">{{ item.title }}</h3> <h3 *ngIf="item.type === 'category'">{{ item.title }}</h3>
<div *ngIf="item.type !== 'category'" class="endpoint-container" id="{{ item.fragment }}"> <div *ngIf="item.type !== 'category'" class="endpoint-container" id="{{ item.fragment }}">
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}"><table><tr><td>{{ item.title }}</td><td><span>{{ item.category }}</span></td></tr></table></a> <a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick({event: $event, fragment: item.fragment})"><table><tr><td>{{ item.title }}</td><td><span>{{ item.category }}</span></td></tr></table></a>
<div class="endpoint-content"> <div class="endpoint-content">
<ng-container *ngTemplateOutlet="dict[item.fragment]" class="endpoint"></ng-container> <ng-container *ngTemplateOutlet="dict[item.fragment]" class="endpoint"></ng-container>
</div> </div>
@@ -54,7 +54,7 @@
<div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance )"> <div *ngIf="!item.hasOwnProperty('options') || ( item.hasOwnProperty('options') && item.options.hasOwnProperty('officialOnly') && item.options.officialOnly && officialMempoolInstance )">
<h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3> <h3 *ngIf="( item.type === 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )">{{ item.title }}</h3>
<div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}"> <div *ngIf="( item.type !== 'category' ) && ( item.showConditions.indexOf(network.val) > -1 )" class="endpoint-container" id="{{ item.fragment }}">
<a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick( $event )" [routerLink]="['./']" fragment="{{ item.fragment }}">{{ item.title }} <span>{{ item.category }}</span></a> <a id="{{ item.fragment + '-tab-header' }}" class="section-header" (click)="anchorLinkClick({event: $event, fragment: item.fragment})">{{ item.title }} <span>{{ item.category }}</span></a>
<div class="endpoint-content"> <div class="endpoint-content">
<div class="endpoint"> <div class="endpoint">
<div class="subtitle" i18n="Api docs endpoint">Endpoint</div> <div class="subtitle" i18n="Api docs endpoint">Endpoint</div>

View File

@@ -204,6 +204,7 @@ h3 {
margin: 20px 0 20px 0; margin: 20px 0 20px 0;
font-size: 24px; font-size: 24px;
position: relative; position: relative;
cursor: pointer;
} }
.endpoint-container .section-header:hover { .endpoint-container .section-header:hover {
text-decoration: none; text-decoration: none;

View File

@@ -56,7 +56,10 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
if( this.route.snapshot.fragment ) { if( this.route.snapshot.fragment ) {
this.openEndpointContainer( this.route.snapshot.fragment ); this.openEndpointContainer( this.route.snapshot.fragment );
if (document.getElementById( this.route.snapshot.fragment )) { if (document.getElementById( this.route.snapshot.fragment )) {
document.getElementById( this.route.snapshot.fragment ).scrollIntoView(); let vOffset = ( window.innerWidth <= 992 ) ? 100 : 60;
window.scrollTo({
top: document.getElementById( this.route.snapshot.fragment ).offsetTop - vOffset
});
} }
} }
window.addEventListener('scroll', that.onDocScroll, { passive: true }); window.addEventListener('scroll', that.onDocScroll, { passive: true });
@@ -124,20 +127,13 @@ export class ApiDocsComponent implements OnInit, AfterViewInit {
this.desktopDocsNavPosition = ( window.pageYOffset > 115 ) ? "fixed" : "relative"; this.desktopDocsNavPosition = ( window.pageYOffset > 115 ) ? "fixed" : "relative";
} }
anchorLinkClick( event: any ) { anchorLinkClick( e ) {
let targetId = ""; let targetId = e.fragment;
if( event.target.nodeName === "A" ) { let vOffset = ( window.innerWidth <= 992 ) ? 100 : 60;
targetId = event.target.hash.substring(1); window.scrollTo({
} else { top: document.getElementById( targetId ).offsetTop - vOffset
let element = event.target; });
while( element.nodeName !== "A" ) { window.history.pushState({}, null, document.location.href.split("#")[0] + "#" + targetId);
element = element.parentElement;
}
targetId = element.hash.substring(1);
}
if( this.route.snapshot.fragment === targetId && document.getElementById( targetId )) {
document.getElementById( targetId ).scrollIntoView();
}
this.openEndpointContainer( targetId ); this.openEndpointContainer( targetId );
} }

View File

@@ -154,7 +154,7 @@ export class StateService {
searchFocus$: Subject<boolean> = new Subject<boolean>(); searchFocus$: Subject<boolean> = new Subject<boolean>();
menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false); menuOpen$: BehaviorSubject<boolean> = new BehaviorSubject(false);
activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [] }); activeGoggles$: BehaviorSubject<ActiveFilter> = new BehaviorSubject({ mode: 'and', filters: [], gradient: 'fee' });
constructor( constructor(
@Inject(PLATFORM_ID) private platformId: any, @Inject(PLATFORM_ID) private platformId: any,

View File

@@ -11,9 +11,12 @@ export interface Filter {
export type FilterMode = 'and' | 'or'; export type FilterMode = 'and' | 'or';
export type GradientMode = 'fee' | 'age';
export interface ActiveFilter { export interface ActiveFilter {
mode: FilterMode, mode: FilterMode,
filters: string[], filters: string[],
gradient: GradientMode,
} }
// binary flags for transaction classification // binary flags for transaction classification