Merge branch 'master' into natsoni/accelerate-address-page

This commit is contained in:
Mononaut 2025-12-21 09:12:42 +00:00
commit eff91d9f88
No known key found for this signature in database
GPG key ID: A3F058E41374C04E
10 changed files with 69 additions and 40 deletions

View file

@ -17,7 +17,7 @@
"crypto-js": "~4.2.0",
"express": "~4.22.1",
"maxmind": "~4.3.11",
"mysql2": "~3.14.1",
"mysql2": "~3.16.0",
"redis": "^4.7.0",
"rust-gbt": "file:./rust-gbt",
"socks-proxy-agent": "~7.0.0",
@ -5496,14 +5496,14 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz",
"integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==",
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz",
"integrity": "sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==",
"dependencies": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"iconv-lite": "^0.7.0",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
@ -5515,14 +5515,18 @@
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/named-placeholders": {
@ -11060,14 +11064,14 @@
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mysql2": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.1.tgz",
"integrity": "sha512-7ytuPQJjQB8TNAYX/H2yhL+iQOnIBjAMam361R7UAL0lOVXWjtdrmoL9HYKqKoLp/8UUTRcvo1QPvK9KL7wA8w==",
"version": "3.16.0",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.16.0.tgz",
"integrity": "sha512-AEGW7QLLSuSnjCS4pk3EIqOmogegmze9h8EyrndavUQnIUcfkVal/sK7QznE+a3bc6rzPbAiui9Jcb+96tPwYA==",
"requires": {
"aws-ssl-profiles": "^1.1.1",
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"iconv-lite": "^0.7.0",
"long": "^5.2.1",
"lru.min": "^1.0.0",
"named-placeholders": "^1.1.3",
@ -11076,9 +11080,9 @@
},
"dependencies": {
"iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
"requires": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
}

View file

@ -48,7 +48,7 @@
"crypto-js": "~4.2.0",
"express": "~4.22.1",
"maxmind": "~4.3.11",
"mysql2": "~3.14.1",
"mysql2": "~3.16.0",
"rust-gbt": "file:./rust-gbt",
"redis": "^4.7.0",
"socks-proxy-agent": "~7.0.0",

View file

@ -1,14 +1,11 @@
import { Application, Request, Response } from 'express';
import config from '../../config';
import pricesUpdater from '../../tasks/price-updater';
import logger from '../../logger';
import PricesRepository from '../../repositories/PricesRepository';
class PricesRoutes {
public initRoutes(app: Application): void {
app
.get(config.MEMPOOL.API_URL_PREFIX + 'prices', this.$getCurrentPrices.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'internal/usd-price-history', this.$getAllPrices.bind(this))
;
}
@ -19,23 +16,6 @@ class PricesRoutes {
res.json(pricesUpdater.getLatestPrices());
}
private async $getAllPrices(req: Request, res: Response): Promise<void> {
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 360_0000 / config.MEMPOOL.PRICE_UPDATES_PER_HOUR).toUTCString());
try {
const usdPriceHistory = await PricesRepository.$getPricesTimesAndId();
const responseData = usdPriceHistory.map(p => {
return { time: p.time, USD: p.USD };
});
res.status(200).json(responseData);
} catch (e: any) {
logger.err(`Exception ${e} in PricesRoutes::$getAllPrices. Code: ${e.code}. Message: ${e.message}`);
res.status(403).send();
}
}
}
export default new PricesRoutes();

View file

@ -3,6 +3,13 @@ __MEMPOOL_BACKEND_MAINNET_HTTP_HOST__=${BACKEND_MAINNET_HTTP_HOST:=127.0.0.1}
__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__=${BACKEND_MAINNET_HTTP_PORT:=8999}
__MEMPOOL_FRONTEND_HTTP_PORT__=${FRONTEND_HTTP_PORT:=8080}
__PROXIED_SERVICES__=${PROXIED_SERVICES:=false}
__PROXIED_SERVICES_HOST__=${PROXIED_SERVICES_HOST:=https://mempool.space}
if [ "${__PROXIED_SERVICES__}" = "true" ]; then
sed -i "s|proxy_pass https://mempool.space;|proxy_pass ${__PROXIED_SERVICES_HOST__};|g" /etc/nginx/conf.d/nginx-mempool.conf
fi
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__/${__MEMPOOL_BACKEND_MAINNET_HTTP_HOST__}/g" /etc/nginx/conf.d/nginx-mempool.conf
sed -i "s/__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__/${__MEMPOOL_BACKEND_MAINNET_HTTP_PORT__}/g" /etc/nginx/conf.d/nginx-mempool.conf

View file

@ -31,10 +31,8 @@
"@types/qrcode": "~1.5.0",
"bootstrap": "~4.6.2",
"clipboard": "^2.0.11",
"cypress": "15.8.1",
"domino": "^2.1.6",
"echarts": "~5.4.0",
"esbuild": "^0.25.8",
"ngx-echarts": "~20.0.2",
"ngx-infinite-scroll": "^20.0.0",
"qrcode": "1.5.1",

View file

@ -85,7 +85,6 @@
"ngx-infinite-scroll": "^20.0.0",
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.25.8",
"tslib": "~2.8.0",
"zone.js": "~0.15.1"
},

View file

@ -569,7 +569,7 @@ export class BlockComponent implements OnInit, OnDestroy {
} else {
this.showPreviousBlocklink = true;
}
if (this.latestBlock.height && this.latestBlock.height === this.blockHeight) {
if (this.latestBlock.height != null && this.latestBlock.height === this.blockHeight) {
this.showNextBlocklink = false;
} else {
this.showNextBlocklink = true;

View file

@ -31,6 +31,21 @@
</div>
<div class="clearfix"></div>
@if (duplicateTxBlocks) {
<div class="box duplicate-tx-alert">
<div class="icon-container">
<fa-icon [icon]="['fas', 'info-circle']" [fixedWidth]="true"></fa-icon>
</div>
<span>
<ng-container i18n="transaction.duplicate.notice|Duplicate transaction notice">
This transaction ID appears twice in the blockchain as the coinbase of blocks <a [routerLink]="['/block/' | relativeUrl, duplicateTxBlocks[0]]">{{ duplicateTxBlocks[0] }}</a> and <a [routerLink]="['/block/' | relativeUrl, duplicateTxBlocks[1]]">{{ duplicateTxBlocks[1] }}</a>, due to a bug later mitigated by <a href="https://github.com/bitcoin/bips/blob/master/bip-0030.mediawiki" target="_blank" rel="noopener">BIP-30</a>.
</ng-container>
<span class="mr-3"></span>
<a href="https://blog.bitmex.com/bitcoins-duplicate-transactions/" target="_blank" rel="noopener" i18n="transaction.duplicate.read-more|Duplicate transaction read more" style="white-space: nowrap;">Read more</a>
</span>
</div>
}
@if (!error) {
<app-transaction-details
[network]="network"

View file

@ -317,3 +317,19 @@
scroll-margin-top: 65px;
}
}
.duplicate-tx-alert {
display: flex;
align-items: center;
gap: 1rem;
border-left: 3px solid var(--info);
margin-bottom: 1.5rem;
.icon-container {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--info);
}
}

View file

@ -57,6 +57,12 @@ export interface TxAuditStatus {
firstSeen?: number;
}
// Known duplicate transaction IDs from early Bitcoin history (pre-BIP-30)
const DUPLICATE_TX_BLOCKS: Record<string, [number, number]> = {
'e3bf3d07d4b0375638d5f1db5255fe07ba2c4cb067cd81b84ee974b6585fb468': [91722, 91880],
'd5d27987d2a3dfc724e359870c6644b40e497bdc0589a033220fe15429d88599': [91812, 91842],
};
@Component({
selector: 'app-transaction',
templateUrl: './transaction.component.html',
@ -142,6 +148,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
waitingForAccelerationInfo: boolean = false;
isLoadingFirstSeen = false;
notAcceleratedOnLoad: boolean = null;
duplicateTxBlocks: [number, number] | undefined;
featuresEnabled: boolean;
segwitEnabled: boolean;
@ -584,6 +591,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
});
}
}
if (this.network === '' && this.txId) {
this.duplicateTxBlocks = DUPLICATE_TX_BLOCKS[this.txId?.toLowerCase()];
}
if (window.innerWidth <= 767.98) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), this.txId], {
queryParamsHandling: 'merge',