Merge branch 'master' into dependabot/npm_and_yarn/frontend/cypress-15.7.0

This commit is contained in:
Felipe Knorr Kuhn 2025-11-28 18:57:04 -08:00 committed by GitHub
commit 7c23c8427d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 4915 additions and 10192 deletions

8587
backend/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -59,15 +59,19 @@
"@types/compression": "^1.7.2",
"@types/crypto-js": "^4.1.1",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.0",
"@types/jest": "^30.0.0",
"@types/ws": "~8.5.10",
"@typescript-eslint/eslint-plugin": "^5.55.0",
"@typescript-eslint/parser": "^5.55.0",
"eslint": "^8.36.0",
"eslint-config-prettier": "^8.8.0",
"jest": "^29.5.0",
"jest": "^30.0.0",
"prettier": "^3.0.0",
"ts-jest": "^29.1.1",
"ts-jest": "^29.4.5",
"ts-node": "^10.9.1"
},
"overrides": {
"js-yaml": "^4.1.1",
"glob": "^11.1.0"
}
}

View file

@ -1,52 +1,55 @@
[
{
"txid": "50136231cb7eeeffb17fc41d1cca213426abe5bf3760e3d6421cad0c0edad367",
"version": 1,
"locktime": 0,
"vin": [
{
"txid": "c7f86fb7b830124057475b282809f3474ef3565daa3de0b599980fb9e84ab019",
"vout": 4217,
"prevout": {
"scriptpubkey": "001466197b5eadd8067ec194a457e1044b6d1fbdd3b3",
"scriptpubkey_asm": "OP_0 OP_PUSHBYTES_20 66197b5eadd8067ec194a457e1044b6d1fbdd3b3",
"scriptpubkey_type": "v0_p2wpkh",
"scriptpubkey_address": "bc1qvcvhkh4dmqr8asv553t7zpztd50mm5ang4na33",
"value": 106
},
"scriptsig": "",
"scriptsig_asm": "",
"witness": [
"3043021f2af6060a142c6cfd7428adad6a50745d2424813d7ced5c0bbcca85e70de1be022021440ca1c8c3ed49ecd1b64dca6911adcd430c5d3dd60d77ffe0072953999f5b01",
"02ead5c34e3d2c506574b562f857576e11380b6ba15d9f0ad7b7303fdaa9c1513d"
],
"is_coinbase": false,
"sequence": 4294967295
}
],
"vout": [
{
"scriptpubkey": "6a023a29",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_2 3a29",
"scriptpubkey_type": "op_return",
"value": 0
{
"txid": "f859a4e6692c3f15a279449eac191ecb9d17d2d6c003bbae9a55e5da07147eab",
"version": 1,
"locktime": 0,
"size": 170,
"weight": 371,
"fee": 11586,
"vin": [
{
"is_coinbase": false,
"prevout": {
"value": 11586,
"scriptpubkey": "512088939b42f2ed113dd4756055b179cd784c2cfa52cad29046411c28335aa2055c",
"scriptpubkey_address": "bc1p3zfekshja5gnm4r4vp2mz7wd0pxze7jjetffq3jprs5rxk4zq4wqesykl2",
"scriptpubkey_asm": "OP_PUSHNUM_1 OP_PUSHBYTES_32 88939b42f2ed113dd4756055b179cd784c2cfa52cad29046411c28335aa2055c",
"scriptpubkey_type": "v1_p2tr"
},
"scriptsig": "",
"scriptsig_asm": "",
"sequence": 4294967295,
"txid": "9b93dab94a3334b6119f75d107da6bcef9435fa52ec6c0de14aa8a094794551a",
"vout": 0,
"witness": [
"6840b6fa27a00ba001cc92797ce4f3ab7b7a32c21d1fce49e893b42e506bd92e8db187966a84ef799915cf671334cc59779915b192bfb66b2afcf384bb61d0f4",
"500049276d20616e20616e6e6578212041726520796f7520616e20616e6e65783f00"
],
"inner_redeemscript_asm": "",
"inner_witnessscript_asm": ""
}
],
"vout": [
{
"value": 0,
"scriptpubkey": "6a05616e6e6578",
"scriptpubkey_address": "",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_5 616e6e6578",
"scriptpubkey_type": "op_return"
}
],
"status": {
"confirmed": true,
"block_height": 896088,
"block_hash": "0000000000000000000148370c6ad0eceb23d0de54ea86a362679cee7fcd3f4a",
"block_time": 1746868490
},
{
"scriptpubkey": "6a036d7648",
"scriptpubkey_asm": "OP_RETURN OP_PUSHBYTES_3 6d7648",
"scriptpubkey_type": "op_return",
"value": 0
}
],
"size": 186,
"weight": 420,
"sigops": 1,
"fee": 106,
"status": {
"confirmed": true,
"block_height": 836361,
"block_hash": "0000000000000000000341cc26cda4af82cd25f7063c448772228cbf2836915b",
"block_time": 1711448028
"order": 2877166599,
"vsize": 93,
"adjustedVsize": 92.75,
"sigops": 0,
"feePerVsize": 124.91644204851752,
"adjustedFeePerVsize": 124.91644204851752,
"effectiveFeePerVsize": 124.91644204851752
}
}
]

View file

@ -24,6 +24,7 @@ const MAX_STANDARD_SCRIPTSIG_SIZE = 1650;
const DUST_RELAY_TX_FEE = 3;
const MAX_OP_RETURN_RELAY = 83;
const DEFAULT_PERMIT_BAREMULTISIG = true;
const MAX_TX_LEGACY_SIGOPS = 2_500 * 4; // witness-adjusted sigops
export class Common {
static nativeAssetId = config.MEMPOOL.NETWORK === 'liquidtestnet' ?
@ -224,6 +225,11 @@ export class Common {
return true;
}
// legacy sigops
if (this.isNonStandardLegacySigops(tx, height)) {
return true;
}
// input validation
for (const vin of tx.vin) {
if (vin.is_coinbase) {
@ -285,6 +291,7 @@ export class Common {
// output validation
let opreturnCount = 0;
let opreturnBytes = 0;
for (const vout of tx.vout) {
// scriptpubkey
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
@ -308,10 +315,7 @@ export class Common {
}
} else if (vout.scriptpubkey_type === 'op_return') {
opreturnCount++;
if ((vout.scriptpubkey.length / 2) > MAX_OP_RETURN_RELAY) {
// over default datacarrier limit
return true;
}
opreturnBytes += vout.scriptpubkey.length / 2;
}
// dust
// (we could probably hardcode this for the different output types...)
@ -333,9 +337,11 @@ export class Common {
}
}
// multi-op-return
if (opreturnCount > 1) {
return true;
// op_return
if (opreturnCount > 0) {
if (!this.isStandardOpReturn(opreturnBytes, opreturnCount, height)) {
return true;
}
}
// TODO: non-mandatory-script-verify-flag
@ -428,6 +434,49 @@ export class Common {
return false;
}
// OP_RETURN size & count limits were lifted in v28.3/v29.2/v30.0
static OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 108_000,
'testnet': 4_750_000,
'signet': 276_500,
'': 921_000,
};
static MAX_DATACARRIER_BYTES = 83;
static isStandardOpReturn(bytes: number, outputs: number,height?: number): boolean {
if (
(height == null || (
this.OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height >= this.OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
)) // limits lifted
|| // OR
(bytes <= this.MAX_DATACARRIER_BYTES && outputs <= 1) // below old limits
) {
return true;
}
return false;
}
// New legacy sigops limit started to be enforced in v30.0
static LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 108_000,
'testnet': 4_750_000,
'signet': 276_500,
'': 921_000,
};
static isNonStandardLegacySigops(tx: TransactionExtended, height?: number): boolean {
if (
height == null || (
this.LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
&& height >= this.LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT[config.MEMPOOL.NETWORK]
)
) {
if (!transactionUtils.checkSigopsBIP54(tx, MAX_TX_LEGACY_SIGOPS)) {
return true;
}
}
return false;
}
static getNonWitnessSize(tx: TransactionExtended): number {
let weight = tx.weight;
let hasWitness = false;

View file

@ -145,6 +145,9 @@ class TransactionUtils {
return str;
}
/**
* Calculate the witness-adjusted sigops cost of an asm script
*/
public countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
if (!script?.length) {
return 0;
@ -213,6 +216,41 @@ class TransactionUtils {
return sigops;
}
/**
* see https://github.com/bitcoin/bitcoin/blob/25c45bb0d0bd6618ec9296a1a43605657124e5de/src/policy/policy.cpp#L166-L193
* returns true if the transactions is permitted under bip54 sigops rules
*
* "Unlike the existing block wide sigop limit which counts sigops present in the block
* itself (including the scriptPubKey which is not executed until spending later), BIP54
* counts sigops in the block where they are potentially executed (only).
* This means sigops in the spent scriptPubKey count toward the limit.
* `fAccurate` means correctly accounting sigops for CHECKMULTISIGs(VERIFY) with 16 pubkeys
* or fewer. This method of accounting was introduced by BIP16, and BIP54 reuses it.
* The GetSigOpCount call on the previous scriptPubKey counts both bare and P2SH sigops."
*/
public checkSigopsBIP54(tx: TransactionExtended, limit): boolean {
let sigops = 0;
for (const input of tx.vin) {
if (input.scriptsig_asm) {
sigops += this.countScriptSigops(input.scriptsig_asm);
}
if (input.prevout) {
// P2SH redeem script
if (input.prevout.scriptpubkey_type === 'p2sh' && input.inner_redeemscript_asm) {
sigops += this.countScriptSigops(input.inner_redeemscript_asm);
} else {
// prevout scriptpubkey
sigops += this.countScriptSigops(input.prevout.scriptpubkey_asm);
}
}
if (sigops > limit) {
return false;
}
}
return true;
}
// returns the most significant 4 bytes of the txid as an integer
public txidToOrdering(txid: string): number {
return parseInt(

View file

@ -181,8 +181,8 @@
"inject": false
},
{
"input": "src/theme-wiz.scss",
"bundleName": "wiz",
"input": "src/theme-softsimon.scss",
"bundleName": "softsimon",
"inject": false
},
{

File diff suppressed because it is too large Load diff

View file

@ -32,13 +32,10 @@
"start:local-esplora": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-esplora",
"start:local-prod": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-prod",
"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-dev && 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",
"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",
"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-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",
"lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix",
@ -60,28 +57,26 @@
"cypress:run:ci:parameterized": "node update-config.js TESTNET_ENABLED=true TESTNET4_ENABLED=true SIGNET_ENABLED=true LIQUID_ENABLED=true ITEMS_PER_PAGE=25 && npm run generate-config && start-server-and-test serve:parameterized 4200 cypress:run:record"
},
"dependencies": {
"@angular-devkit/build-angular": "^20.3.7",
"@angular/animations": "^20.3.9",
"@angular/cli": "^20.3.7",
"@angular/common": "^20.3.9",
"@angular/compiler": "^20.3.9",
"@angular/core": "^20.3.9",
"@angular/forms": "^20.3.9",
"@angular/localize": "^20.3.9",
"@angular/platform-browser": "^20.3.9",
"@angular/platform-browser-dynamic": "^20.3.9",
"@angular/platform-server": "^20.3.9",
"@angular/router": "^20.3.9",
"@angular/ssr": "^20.3.7",
"@angular-devkit/build-angular": "^20.3.12",
"@angular/animations": "^20.3.14",
"@angular/cli": "^20.3.12",
"@angular/common": "^20.3.14",
"@angular/compiler": "^20.3.14",
"@angular/core": "^20.3.14",
"@angular/forms": "^20.3.14",
"@angular/localize": "^20.3.14",
"@angular/platform-browser": "^20.3.14",
"@angular/platform-browser-dynamic": "^20.3.14",
"@angular/platform-server": "^20.3.14",
"@angular/router": "^20.3.14",
"@angular/ssr": "^20.3.12",
"@fortawesome/angular-fontawesome": "^3.0.0",
"@fortawesome/fontawesome-common-types": "~6.7.2",
"@fortawesome/fontawesome-svg-core": "~6.7.2",
"@fortawesome/free-solid-svg-icons": "~6.7.2",
"@mempool/mempool.js": "2.3.0",
"@ng-bootstrap/ng-bootstrap": "^19.0.0",
"@types/qrcode": "~1.5.0",
"bootstrap": "~4.6.2",
"browserify": "^17.0.0",
"clipboard": "^2.0.11",
"domino": "^2.1.6",
"echarts": "~5.4.0",
@ -90,14 +85,13 @@
"qrcode": "1.5.1",
"rxjs": "~7.8.1",
"esbuild": "^0.25.8",
"tinyify": "^4.0.0",
"tlite": "^0.1.9",
"tslib": "~2.8.0",
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular/compiler-cli": "^20.3.9",
"@angular/language-service": "^20.3.9",
"@angular/compiler-cli": "^20.3.14",
"@angular/language-service": "^20.3.14",
"@types/node": "^24.9.2",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",

View file

@ -427,16 +427,11 @@
</div>
</ng-container>
<div class="maintainers" id="project-maintainers">
<h3 i18n="about.maintainers">Project Maintainers</h3>
<div class="managers" id="project-managers">
<h3 i18n="about.project-manager">Powered By</h3>
<div class="wrapper">
<a href="https://x.com/softsimon_" target="_blank" title="softsimon">
<img class="image" src="/resources/profile/softsimon.jpg" />
<span>softsimon</span>
</a>
<a href="https://x.com/wiz" target="_blank" title="wiz">
<img class="image" src="/resources/profile/wiz.png" />
<span>wiz</span>
<a href="https://wiz.biz" target="_blank" title="wiz & associates">
<img class="wiz-logo" src="/resources/wiz.png" />
</a>
</div>
</div>
@ -444,7 +439,7 @@
<div class="copyright">
<div class="title">
Copyright &copy; 2019-2025<br>
Mempool Space K.K.<br>
Mempool Holdings S.A. de C.V.<br>
and other shadowy super-coders
</div>
<p>
@ -460,7 +455,7 @@
Trademark Notice<br>
</div>
<p>
The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Wallet&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries.
The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Wallet&trade;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool Logo, the mempool Square Logo, the mempool block visualization Logo, the mempool Blocks Logo, the mempool transaction Logo, the mempool Blocks 3 | 2 Logo, the mempool research Logo, the mempool.space Vertical Logo, and the mempool.space Horizontal Logo are either registered trademarks or trademarks of Mempool Holdings S.A. de C.V. in Japan, the United States, and/or other countries.
</p>
<p>
While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. See our <a href="https://mempool.space/trademark-policy">Trademark Policy and Guidelines</a> for more details, published on &lt;https://mempool.space/trademark-policy&gt;.

View file

@ -57,7 +57,8 @@
.alliances,
.enterprise-sponsor,
.community-integrations-sponsor,
.maintainers {
.maintainers,
.managers {
margin-top: 30px;
margin-bottom: 68px;
scroll-margin: 30px;
@ -67,7 +68,8 @@
}
}
.maintainers {
.maintainers,
.managers {
margin-bottom: 50px;
}
@ -259,6 +261,12 @@
height: 64px;
}
.wiz-logo {
width: 260px;
max-width: 90vw;
margin: auto;
}
.enterprise-sponsor {
.wrapper {
display: flex;

View file

@ -285,8 +285,8 @@ export class AddressComponent implements OnInit, OnDestroy {
}
this.isLoadingTransactions = false;
let addressVin: Vin[] = [];
let vinIds: string[] = [];
const addressVin: Vin[] = [];
const vinIds: string[] = [];
for (const tx of this.transactions) {
tx.vin.forEach((v, index) => {
if (v.prevout?.scriptpubkey_address === this.address.address) {
@ -296,7 +296,7 @@ export class AddressComponent implements OnInit, OnDestroy {
});
}
this.addressTypeInfo.processInputs(addressVin, vinIds);
this.hasTapTree = this.addressTypeInfo.tapscript && this.addressTypeInfo.scripts.values().next().value.scriptPath.length / 2 > 33;
this.hasTapTree = this.addressTypeInfo.tapscript && this.addressTypeInfo.scripts.values().next().value.taprootInfo.scriptPath.merkleBranches.length > 0;
// hack to trigger change detection
this.addressTypeInfo = this.addressTypeInfo.clone();

View file

@ -17,9 +17,9 @@
<br>
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Holdings S.A. de C.V. in El Salvador ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Holdings S.A. de C.V. in El Salvador ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
<br>

View file

@ -8,7 +8,7 @@
<ng-container *ngIf="chainTips$ | async as chainTips">
<div *ngFor="let chainTip of chainTips" class="chain-tip">
<p class="info">
<span class="height"><a [routerLink]="['/block', chainTip.canonical?.id || chainTip.height]">{{ chainTip.height }}</a></span>
<span class="height"><a [routerLink]="[('/block/' + (chainTip.canonical?.id || chainTip.height)) | relativeUrl]">{{ chainTip.height }}</a></span>
<span class="badges">
<span class="type" [ngSwitch]="chainTip.status">
<span *ngSwitchCase="'headers-only'" class="badge badge-info" i18n="chain-tips.headers-only">Headers Only</span>
@ -25,7 +25,7 @@
<div class="block-column stale">
<div class="block-label" i18n="chain-tips.stale-block"><span class="badge badge-secondary">Stale</span></div>
<div class="block-wrapper">
<div class="bitcoin-block mined-block stale-block" [style.background]="getBlockGradient(chainTip.stale)" [routerLink]="['/block/' + chainTip.stale.id]"></div>
<div class="bitcoin-block mined-block stale-block" [style.background]="getBlockGradient(chainTip.stale)" [routerLink]="[('/block/' + chainTip.stale.id) | relativeUrl]"></div>
<div class="block-body">
<div *ngIf="chainTip.stale?.extras; else emptyStaleBlockFees" class="fees">
~<app-fee-rate [fee]="chainTip.stale.extras.medianFee" unitClass="" rounding="1.0-0"></app-fee-rate>
@ -71,7 +71,7 @@
<div class="block-column canonical">
<div class="block-label" i18n="chain-tips.winning-block"><span class="badge badge-success">Winning</span></div>
<div class="bitcoin-block mined-block canonical-block" [style.background]="getBlockGradient(chainTip.canonical)" [routerLink]="['/block/' + chainTip.canonical.id]">
<div class="bitcoin-block mined-block canonical-block" [style.background]="getBlockGradient(chainTip.canonical)" [routerLink]="[('/block/' + chainTip.canonical.id) | relativeUrl]">
<div class="block-body">
<div *ngIf="chainTip.canonical?.extras; else emptyCanonicalBlockFees" class="fees">
~<app-fee-rate [fee]="chainTip.canonical.extras.medianFee" unitClass="" rounding="1.0-0"></app-fee-rate>

View file

@ -78,16 +78,12 @@ export class TaprootAddressScriptsComponent implements OnChanges {
const merklePaths: { leafVersion: number, merklePath: string[] }[] = [];
this.depth = 0;
for (const script of scripts) {
const controlBlock = script.scriptPath;
const m = ((controlBlock.length / 2) - 33) / 32;
if (!Number.isInteger(m) || m <= 0) {
const scriptInfo = script.taprootInfo;
if (!scriptInfo.scriptPath?.merkleBranches?.length) {
throw new Error("Merkle path length must be >= 1");
}
const leafVersion = parseInt(controlBlock.slice(0, 2), 16) & 0xfe;
const merklePath = [];
for (let i = 0; i < m; i++) {
merklePath.push(controlBlock.slice(66 + i * 64, 66 + (i + 1) * 64));
}
const leafVersion = scriptInfo.scriptPath.leafVersion;
const merklePath = scriptInfo.scriptPath.merkleBranches.slice();
if (merklePath.length > this.depth) {
this.depth = merklePath.length;
}

View file

@ -11,9 +11,9 @@
<div class="text-left">
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Space K.K. in Japan ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<p *ngIf="officialMempoolSpace">The <a href="https://mempool.space/">mempool.space</a> website, the <a href="https://liquid.network/">liquid.network</a> website, the <a href="https://bitcoin.gob.sv/">bitcoin.gob.sv</a> website, their associated API services, and related network and server infrastructure (collectively, the "Website") are operated by Mempool Holdings S.A. de C.V. in El Salvador ("Mempool", "We", or "Us") and self-hosted from <a href="https://bgp.tools/as/142052#connectivity">AS142052</a>.</p>
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Space K.K. in Japan ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
<p *ngIf="!officialMempoolSpace">This website and its API service (collectively, the "Website") are operated by a member of the Bitcoin community ("We" or "Us"). Mempool Holdings S.A. de C.V. in El Salvador ("Mempool") has no affiliation with the operator of this Website, and does not sponsor or endorse the information provided herein.</p>
<h5>By accessing this Website, you agree to the following Terms of Service:</h5>

View file

@ -1,7 +1,7 @@
<div [formGroup]="themeForm" class="text-small text-center">
<select formControlName="theme" class="custom-select custom-select-sm form-control-secondary form-control mx-auto" (change)="changeTheme()">
<option value="default" i18n="mempool-goggles.classic">Classic</option>
<option value="default">wiz</option>
<option value="contrast">BlueMatt</option>
<option value="wiz">wiz</option>
<option value="softsimon">softsimon</option>
</select>
</div>

View file

@ -12,7 +12,7 @@ import { Subscription } from 'rxjs';
})
export class ThemeSelectorComponent implements OnInit {
themeForm: UntypedFormGroup;
themes = ['default', 'contrast', 'wiz', 'bukele'];
themes = ['default', 'contrast', 'softsimon', 'bukele'];
themeSubscription: Subscription;
constructor(

View file

@ -1,7 +1,7 @@
<div class="container-xl trademark-policy">
<div *ngIf="!officialMempoolSpace">
<p>This website is operated by an individual member of the Bitcoin community, and is not affiliated with Mempool Space K.K.</p>
<p>This website is operated by an individual member of the Bitcoin community, and is not affiliated with Mempool Holdings S.A. de C.V.</p>
<p>Please view the Trademark Policy and Guidelines for The Mempool Open Source Project at <a href="https://mempool.space/trademark-policy">https://mempool.space/trademark-policy</a>.</p>
</div>
@ -19,7 +19,7 @@
<br>
<p>Thank you for your interest in The Mempool Open Source Project (“Project”). While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. This Policy (the “Policy”) outlines the allowable use of all trademarks and service marks, whether word marks, logos or trade dress (collectively, the "Marks"), owned by Mempool Space K.K. (“Mempool”).</p>
<p>Thank you for your interest in The Mempool Open Source Project (“Project”). While our software is available under an open source software license, the copyright license does not include an implied right or license to use our trademarks. This Policy (the “Policy”) outlines the allowable use of all trademarks and service marks, whether word marks, logos or trade dress (collectively, the "Marks"), owned by Mempool Holdings S.A. de C.V. (“Mempool”).</p>
<p>The role of trademarks is to provide assurance about the quality of the products or services with which the trademark is associated. Therefore, because an open source license allows your unrestricted modification of the copyrighted software, we cannot be sure that your modifications to the software are ones that will not be misleading if distributed under the same name. Instead, this Policy describes the circumstances under which you may use our Marks.</p>
@ -55,6 +55,7 @@
<table class="table table-bordered">
<tbody>
<tr><td>Mempool Space K.K.</td></tr>
<tr><td>Mempool Holdings S.A. de C.V.</td></tr>
<tr><td>The Mempool Open Source Project</td></tr>
<tr><td>Mempool Accelerator</td></tr>
<tr><td>Mempool Enterprise</td></tr>
@ -341,7 +342,7 @@
<p>Also, if you are using our Marks in a way described in the sections "Uses for Which We Are Granting a License," you must include the following trademark attribution at the foot of the webpage where you have used the Mark (or, if in a book, on the credits page), on any packaging or labeling, and on advertising or marketing materials:</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Wallet&trade;, Mempool&reg;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Space K.K in Japan, the United States, and/or other countries, and are used with permission. Mempool Space K.K. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<p>"The Mempool Open Source Project&reg;, Mempool Accelerator&reg;, Mempool Enterprise&reg;, Mempool Wallet&trade;, Mempool&reg;, mempool.space&reg;, Be your own explorer&trade;, Explore the full Bitcoin ecosystem&reg;, Mempool Goggles&trade;, the mempool logo;, the mempool Square logo;, the mempool Blocks logo;, the mempool Blocks 3 | 2 logo;, the mempool.space Vertical Logo;, the Mempool Accelerator logo;, the Mempool Goggles logo;, and the mempool.space Horizontal logo are either registered trademarks or trademarks of Mempool Holdings S.A. de C.V. in Japan, the United States, and/or other countries, and are used with permission. Mempool Holdings S.A. de C.V.. has no affiliation with and does not sponsor or endorse the information provided herein."</p>
<li>What to Do When You See Abuse</li>

View file

@ -20,7 +20,7 @@ export class TrademarkPolicyComponent {
ngOnInit(): void {
this.seoService.setTitle('Trademark Policy');
this.seoService.setDescription('An overview of the trademarks registered by Mempool Space K.K. and The Mempool Open Source Project® and what we consider to be lawful usage of those trademarks.');
this.seoService.setDescription('An overview of the trademarks registered by Mempool Holdings S.A. de C.V. and The Mempool Open Source Project® and what we consider to be lawful usage of those trademarks.');
this.ogService.setManualOgImage('trademark-policy.jpg');
}
}

View file

@ -158,7 +158,7 @@
<td style="text-align: left;">
<app-asm
[asm]="vin.scriptsig_asm"
[annotations]="{ signatures: tx['_sigmap'], selectedSig: selectedSig?.txIndex === i && selectedSig?.vindex === vindex ? selectedSig.sig : null }"
[annotations]="{ signatures: tx['_sigmap'], selectedSig: selectedSig?.txIndex === i && selectedSig?.vindex === vindex ? selectedSig.sig : null, p2sh: vin.prevout?.scriptpubkey_type === 'p2sh' && !vin.witness }"
(showSigInfo)="showSigInfo(i, vindex, $event)"
(hideSigInfo)="hideSigInfo()"
></app-asm>
@ -183,32 +183,43 @@
<fa-icon [icon]="['fas', 'key']" [fixedWidth]="true"></fa-icon>
</span>
</ng-container>
@if (vin.witness.length > 1 && windex === vin.witness.length - 1 && witness.slice(0, 2) === '50') {
@if (windex === vin.taprootInfo?.controlBlockIndex) {
<span class="control-block" ngbTooltip="taproot control block" (click)="toggleTaprootControlBlock(vindex)">
<fa-icon [icon]="['fas', 'code-fork']" [fixedWidth]="true" [rotate]="180"></fa-icon>
</span>
<ng-container *ngTemplateOutlet="taprootControlBlock; context: { $implicit: vin.taprootInfo?.scriptPath, controlBlock: witness, truncate: !showFullWitness[vindex][windex] }"></ng-container>
} @else if (windex === vin.taprootInfo?.scriptIndex || (windex === vin.witness.length - 1 && ['p2sh', 'v0_p2wsh'].includes(vin.prevout?.scriptpubkey_type) && vin.scriptsig.length !== 46)) {
<span class="script" ngbTooltip="script">
<fa-icon [icon]="['fas', 'code']" [fixedWidth]="true"></fa-icon>
</span>
} @else if (windex === vin.taprootInfo?.annexIndex) {
<span class="annex" ngbTooltip="annex">
<fa-icon [icon]="['fas', 'paperclip']" [fixedWidth]="true"></fa-icon>
</span>
}
@if (witness.length > 1000) {
@if (!showFullWitness[vindex][windex]) {
{{ witness | slice:0:1000 }}
@if (windex !== vin.taprootInfo?.controlBlockIndex) {
@if (witness.length > 1000) {
@if (!showFullWitness[vindex][windex]) {
{{ witness | slice:0:1000 }}
} @else {
{{ witness }}
}
} @else if (witness) {
<ng-container *ngIf="tx['_sigmap']?.[witness]?.sig.sighash !== 0 && tx['_sigmap']?.[witness] as sigInfo; else plainSig">
<span class="witness">
{{witness.slice(0, -2)}}<span class="sig sighash-{{sigInfo.sig.sighash}}"
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sigInfo.sig"
(mouseenter)="showSigInfo(i, vindex, sigInfo.sig)"
(mouseleave)="hideSigInfo()"
[ngbTooltip]="sighashLabels[sigInfo.sig.sighash]">{{witness.slice(-2)}}</span>
</span>
</ng-container>
<ng-template #plainSig>
{{ witness }}
</ng-template>
} @else {
{{ witness }}
&lt;empty&gt;
}
} @else if (witness) {
<ng-container *ngIf="tx['_sigmap']?.[witness]?.sig.sighash !== 0 && tx['_sigmap']?.[witness] as sigInfo; else plainSig">
<span class="witness">
{{witness.slice(0, -2)}}<span class="sig sighash-{{sigInfo.sig.sighash}}"
[class.hovered]="selectedSig && selectedSig.txIndex === i && selectedSig.vindex === vindex && selectedSig.sig === sigInfo.sig"
(mouseenter)="showSigInfo(i, vindex, sigInfo.sig)"
(mouseleave)="hideSigInfo()"
[ngbTooltip]="sighashLabels[sigInfo.sig.sighash]">{{witness.slice(-2)}}</span>
</span>
</ng-container>
<ng-template #plainSig>
{{ witness }}
</ng-template>
} @else {
&lt;empty&gt;
}
</p>
@if (witness.length > 1000) {
@ -232,6 +243,14 @@
<td i18n="transactions-list.p2sh-redeem-script">P2SH redeem script</td>
<td style="text-align: left;" [innerHTML]="vin.inner_redeemscript_asm | asmStyler"></td>
</tr>
@if (vin.taprootInfo?.scriptPath && showTaprootControlBlock[vindex]) {
<tr>
<td i18n="transactions-list.p2tr-control-block">P2TR control block</td>
<td style="text-align: left; padding: 0px;">
<ng-container *ngTemplateOutlet="controlBlockInfo; context: { $implicit: vin.taprootInfo?.scriptPath }"></ng-container>
</td>
</tr>
}
<tr *ngIf="vin.inner_witnessscript_asm">
@if (isLiquid && vin.inner_simplicityscript) {
<td i18n="transactions-list.p2tr-simplicity-script">P2TR Simplicity script</td>
@ -536,3 +555,37 @@
</ng-template>
<ng-template #xRemaining let-x i18n="x-remaining">{{ x }} remaining</ng-template>
<ng-template #taprootControlBlock let-scriptPath let-controlBlock="controlBlock" let-truncate="truncate">
<span class="leaf-version" ngbTooltip="leaf version and parity"> {{ controlBlock.slice(0, 2) }}</span>
@if (scriptPath.isNUMS) {
<span class="internal-key nums" ngbTooltip="internal key (NUMS point)">{{ scriptPath.internalKey }}</span>
} @else {
<span class="internal-key" ngbTooltip="internal key">{{ scriptPath.internalKey }}</span>
}
<span *ngFor="let branch of scriptPath.merkleBranches.slice(0, truncate ? 14 : scriptPath.merkleBranches.length); index as i" class="merkle-branch" ngbTooltip="taptree merkle branch {{ i + 1 }}">{{ branch }}</span>
</ng-template>
<ng-template #controlBlockInfo let-scriptPath>
<table class="table table-borderless control-block-info">
<tr></tr>
<tr>
<td i18n="transactions-list.taproot.leaf-version">leaf version</td>
<td>{{ scriptPath.leafVersion | decimal2hex:1 }}</td>
</tr>
<tr>
<td i18n="transactions-list.taproot.parity">parity</td>
<td>{{ scriptPath.parity ? '0x01' : '0x00' }}</td>
</tr>
@if (scriptPath.isNUMS) {
<tr>
<td i18n="transactions-list.taproot.keypath">key path</td>
<td>disabled</td>
</tr>
}
<tr>
<td i18n="transactions-list.taproot.script-depth">script depth</td>
<td>{{ scriptPath.merkleBranches.length }}</td>
</tr>
</table>
</ng-template>

View file

@ -156,11 +156,12 @@
transition: transform 0.2s ease, opacity 0.2s ease;
}
.annex {
.annex, .control-block, .script {
margin-right: -5px;
position: relative;
left: 0px;
top: 0px;
cursor: pointer;
}
.mobile-bottomcol {
@ -351,4 +352,42 @@ h2 {
@media (min-width: 1200px) {
max-width: 180px;
}
}
.merkle-branch:nth-child(2n + 1), .internal-key {
color: #cccccc;
font-weight: 500;
}
.merkle-branch, .internal-key, .leaf-version {
&:hover {
color: white;
font-weight: 600;
}
}
.internal-key.nums {
color: #ffdddd;
&:hover {
color: #ffdddd;
}
}
.control-block-info {
padding: 0;
tr td {
width: 50%;
white-space: nowrap;
&:first-child {
color: white;
width: 50%;
white-space: nowrap;
}
@media (max-width: 480px) {
padding: 4px;
}
}
}

View file

@ -15,7 +15,7 @@ import { OrdApiService } from '@app/services/ord-api.service';
import { Inscription } from '@app/shared/ord/inscription.utils';
import { Etching, Runestone } from '@app/shared/ord/rune.utils';
import { ADDRESS_SIMILARITY_THRESHOLD, AddressMatch, AddressSimilarity, AddressType, AddressTypeInfo, checkedCompareAddressStrings, detectAddressType } from '@app/shared/address-utils';
import { processInputSignatures, Sighash, SigInfo, SighashLabels } from '@app/shared/transaction.utils';
import { processInputSignatures, Sighash, SigInfo, SighashLabels, parseTaproot } from '@app/shared/transaction.utils';
import { ActivatedRoute } from '@angular/router';
import { SighashFlag } from '@app/shared/transaction.utils';
@ -68,6 +68,7 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
showFullScriptPubkeyHex: { [voutIndex: number]: boolean } = {};
showFullOpReturnData: { [voutIndex: number]: boolean } = {};
showFullOpReturnPreview: { [voutIndex: number]: boolean } = {};
showTaprootControlBlock: { [vinIndex: number]: boolean } = {};
showOrdData: { [key: string]: { show: boolean; inscriptions?: Inscription[]; runestone?: Runestone, runeInfo?: { [id: string]: { etching: Etching; txid: string; } }; } } = {};
similarityMatches: Map<string, Map<string, { score: number, match: AddressMatch, group: number }>> = new Map();
@ -305,15 +306,6 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
// Check for ord data fingerprints in inputs and outputs
if (this.stateService.network !== 'liquid' && this.stateService.network !== 'liquidtestnet') {
for (let i = 0; i < tx.vin.length; i++) {
if (tx.vin[i].prevout?.scriptpubkey_type === 'v1_p2tr' && tx.vin[i].witness?.length) {
const hasAnnex = tx.vin[i].witness?.[tx.vin[i].witness.length - 1].startsWith('50');
if (tx.vin[i].witness.length > (hasAnnex ? 2 : 1) && tx.vin[i].witness[tx.vin[i].witness.length - (hasAnnex ? 3 : 2)].includes('0063036f7264')) {
tx.vin[i].isInscription = true;
tx.largeInput = true;
}
}
}
for (let i = 0; i < tx.vout.length; i++) {
if (tx.vout[i]?.scriptpubkey?.startsWith('6a5d')) {
tx.vout[i].isRunestone = true;
@ -337,19 +329,19 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
}
}
tx['_showSignatures'] = this.shouldShowSignatures(tx);
} else { // check for simplicity script spends
for (const vin of tx.vin) {
if (vin.prevout?.scriptpubkey_type === 'v1_p2tr' && vin.inner_witnessscript_asm) {
const hasAnnex = vin.witness[vin.witness.length - 1].startsWith('50');
const isScriptSpend = vin.witness.length > (hasAnnex ? 2 : 1);
if (isScriptSpend) {
const controlBlock = hasAnnex ? vin.witness[vin.witness.length - 2] : vin.witness[vin.witness.length - 1];
const scriptHex = hasAnnex ? vin.witness[vin.witness.length - 3] : vin.witness[vin.witness.length - 2]; // bip341 script element
const tapleafVersion = parseInt(controlBlock.slice(0, 2), 16) & 0xfe;
// simplicity script spend
if (tapleafVersion === 0xbe) {
vin.inner_simplicityscript = vin.witness[1]; // simplicity program is the second witness element
}
}
// parse taproot spends
for (const vin of tx.vin) {
if (vin.prevout?.scriptpubkey_type === 'v1_p2tr') {
vin.taprootInfo = parseTaproot(vin.witness);
if (this.stateService.network !== 'liquid' && this.stateService.network !== 'liquidtestnet') {
if (vin.taprootInfo?.scriptPath?.script.includes('0063036f7264')) {
vin.isInscription = true;
tx.largeInput = true;
}
} else {
if (vin.taprootInfo?.scriptPath?.leafVersion === 0xbe) {
vin.inner_simplicityscript = vin.witness[1]; // simplicity program is the second witness element
}
}
}
@ -614,6 +606,10 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
this.showFullOpReturnPreview[voutIndex] = !this.showFullOpReturnPreview[voutIndex];
}
toggleTaprootControlBlock(vinIndex: number): void {
this.showTaprootControlBlock[vinIndex] = !this.showTaprootControlBlock[vinIndex];
}
toggleOrdData(txid: string, type: 'vin' | 'vout', index: number) {
const tx = this.transactions.find((tx) => tx.txid === txid);
if (!tx) {
@ -624,10 +620,9 @@ export class TransactionsListComponent implements OnInit, OnChanges, OnDestroy {
this.showOrdData[key] = this.showOrdData[key] || { show: false };
if (type === 'vin') {
if (!this.showOrdData[key].inscriptions) {
const hasAnnex = tx.vin[index].witness?.[tx.vin[index].witness.length - 1].startsWith('50');
this.showOrdData[key].inscriptions = this.ordApiService.decodeInscriptions(tx.vin[index].witness[tx.vin[index].witness.length - (hasAnnex ? 3 : 2)]);
const tapscript = tx.vin[index].taprootInfo?.scriptPath?.script;
this.showOrdData[key].inscriptions = this.ordApiService.decodeInscriptions(tapscript);
}
this.showOrdData[key].show = !this.showOrdData[key].show;

View file

@ -1,5 +1,6 @@
import { Price } from '@app/services/price.service';
import { IChannel } from '@interfaces/node-api.interface';
import { ParsedTaproot } from '../shared/transaction.utils';
export interface Transaction {
txid: string;
@ -80,6 +81,8 @@ export interface Vin {
isInscription?: boolean;
// temporary field for extracted raw simplicity scripts
inner_simplicityscript?: string;
// parsed taproot info
taprootInfo?: ParsedTaproot;
}
interface Issuance {

View file

@ -2,6 +2,7 @@ import '@angular/localize/init';
import { ScriptInfo } from '@app/shared/script.utils';
import { Vin, Vout } from '@interfaces/electrs.interface';
import { BECH32_CHARS_LW, BASE58_CHARS, HEX_CHARS } from '@app/shared/regex.utils';
import { parseTaproot } from './transaction.utils';
export type AddressType = 'fee'
| 'empty'
@ -158,20 +159,18 @@ export class AddressTypeInfo {
if (this.type === 'v1_p2tr') {
for (let i = 0; i < vin.length; i++) {
const v = vin[i];
const hasAnnex = v.witness[v.witness.length - 1].startsWith('50');
const isScriptSpend = v.witness.length > (hasAnnex ? 2 : 1);
if (isScriptSpend) {
const controlBlock = hasAnnex ? v.witness[v.witness.length - 2] : v.witness[v.witness.length - 1];
const scriptHex = hasAnnex ? v.witness[v.witness.length - 3] : v.witness[v.witness.length - 2];
const tapleafVersion = parseInt(controlBlock.slice(0, 2), 16) & 0xfe;
if (tapleafVersion === 0xc0 && v.inner_witnessscript_asm) {
if (!v.taprootInfo) {
v.taprootInfo = parseTaproot(v.witness);
}
const taprootInfo = v.taprootInfo;
if (taprootInfo.scriptPath) {
if (taprootInfo.scriptPath.leafVersion === 0xc0 && v.inner_witnessscript_asm) {
this.tapscript = true;
this.processScript(new ScriptInfo('inner_witnessscript', scriptHex, v.inner_witnessscript_asm, v.witness, controlBlock, vinIds?.[i]));
} else if (this.network === 'liquid' || this.network === 'liquidtestnet' && tapleafVersion === 0xbe) {
this.processScript(new ScriptInfo('inner_witnessscript', taprootInfo.scriptPath.script, v.inner_witnessscript_asm, v.witness, taprootInfo, vinIds?.[i]));
} else if (this.network === 'liquid' || this.network === 'liquidtestnet' && taprootInfo.scriptPath.leafVersion === 0xbe) {
this.simplicity = true;
v.inner_simplicityscript = v.witness[1];
this.processScript(new ScriptInfo('inner_simplicityscript', scriptHex, null, v.witness, controlBlock, vinIds?.[i]));
this.processScript(new ScriptInfo('inner_simplicityscript', taprootInfo.scriptPath.simplicityScript, null, v.witness, taprootInfo, vinIds?.[i]));
}
}
}

View file

@ -1,7 +1,7 @@
<div class="asm">
@for (instruction of instructions; track instruction.instruction) {
@for (instruction of instructions; track instruction.instruction; let i = $index) {
<span [class]='opcodeStyles.get(instruction.instruction)'>OP_{{instruction.instruction}}</span>
@for (arg of instruction.args; track arg) {
@for (arg of instruction.args; track arg; let j = $index) {
<ng-container *ngIf="annotations.signatures?.[arg] as sigInfo; else plainArg">
<span class="sig sig-key sig-inline sighash-{{sigInfo.sig.sighash}}"
[class.hovered]="annotations.selectedSig && annotations.selectedSig === sigInfo.sig"
@ -22,6 +22,11 @@
</ng-template>
</ng-container>
<ng-template #plainArg>
@if (annotations.p2sh && i === instructions.length - 1) {
<span class="script" ngbTooltip="script">
<fa-icon [icon]="['fas', 'code']" [fixedWidth]="true"></fa-icon>
</span>
}
{{ arg }}
</ng-template>
}

View file

@ -50,4 +50,9 @@
margin-left: 4px;
margin-right: -4px;
}
}
.script {
margin-left: 4px;
margin-right: -4px;
}

View file

@ -13,10 +13,12 @@ export class AsmComponent {
@Input() crop: number = 0;
@Input() annotations: {
signatures: Record<string, { sig: SigInfo, vindex: number }>,
selectedSig: SigInfo | null
selectedSig: SigInfo | null,
p2sh: boolean
} = {
signatures: {},
selectedSig: null
selectedSig: null,
p2sh: false
};
@Output() showSigInfo = new EventEmitter<SigInfo>();
@Output() hideSigInfo = new EventEmitter<void>();

View file

@ -5,7 +5,7 @@ import { Pipe, PipeTransform } from '@angular/core';
standalone: false,
})
export class Decimal2HexPipe implements PipeTransform {
transform(decimal: number): string {
return `0x` + ( decimal.toString(16) ).padStart(8, '0');
transform(decimal: number, bytes: number = 4): string {
return `0x` + ( decimal.toString(16) ).padStart(bytes * 2, '0');
}
}

View file

@ -1,5 +1,6 @@
import { Vin } from "../interfaces/electrs.interface";
import { AddressType, detectAddressType } from "./address-utils";
import { ParsedTaproot } from "./transaction.utils";
const opcodes = {
OP_FALSE: 0,
@ -175,18 +176,18 @@ export const ScriptTemplates: { [type: string]: (...args: any) => ScriptTemplate
export class ScriptInfo {
type: ScriptType;
scriptPath?: string;
taprootInfo?: ParsedTaproot;
hex?: string;
asm?: string;
vinId?: string;
template: ScriptTemplate;
constructor(type: ScriptType, hex?: string, asm?: string, witness?: string[], scriptPath?: string, vinId?: string) {
constructor(type: ScriptType, hex?: string, asm?: string, witness?: string[], taprootInfo?: ParsedTaproot, vinId?: string) {
this.type = type;
this.hex = hex;
this.asm = asm;
if (scriptPath) {
this.scriptPath = scriptPath;
if (taprootInfo) {
this.taprootInfo = taprootInfo;
}
if (vinId) {
this.vinId = vinId;
@ -201,7 +202,7 @@ export class ScriptInfo {
}
get key(): string {
return this.type + (this.scriptPath || '');
return this.type + (this.taprootInfo?.controlBlock || '');
}
}

View file

@ -8,7 +8,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa
faFastForward, faWallet, faUserClock, faWrench, faUserFriends, faQuestionCircle, faHistory, faSignOutAlt, faKey, faSuitcase, faIdCardAlt, faNetworkWired, faUserCheck,
faCircleCheck, faUserCircle, faCheck, faRocket, faScaleBalanced, faHourglassStart, faHourglassHalf, faHourglassEnd, faWandMagicSparkles, faTimeline,
faCircleXmark, faCalendarCheck, faMoneyBillTrendUp, faRobot, faShareNodes, faCreditCard, faMicroscope, faExclamationTriangle, faLockOpen, faPaperclip, faAddressCard,
faMedal, faBug, faFilePdf, faPiggyBank, faLayerGroup, faHeart, faCashRegister } from '@fortawesome/free-solid-svg-icons';
faMedal, faBug, faFilePdf, faPiggyBank, faLayerGroup, faHeart, faCashRegister, faCodeFork, faCode } from '@fortawesome/free-solid-svg-icons';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { MenuComponent } from '@components/menu/menu.component';
import { PreviewTitleComponent } from '@components/master-page-preview/preview-title.component';
@ -484,5 +484,7 @@ export class SharedModule {
library.addIcons(faLayerGroup);
library.addIcons(faHeart);
library.addIcons(faCashRegister);
library.addIcons(faCodeFork);
library.addIcons(faCode);
}
}

View file

@ -20,7 +20,11 @@ const MAX_STANDARD_SCRIPTSIG_SIZE = 1650;
const DUST_RELAY_TX_FEE = 3;
const MAX_OP_RETURN_RELAY = 83;
const DEFAULT_PERMIT_BAREMULTISIG = true;
const MAX_TX_LEGACY_SIGOPS = 2_500 * 4; // witness-adjusted sigops
/**
* Calculate the witness-adjusted sigops cost of an asm script
*/
export function countScriptSigops(script: string, isRawScript: boolean = false, witness: boolean = false): number {
if (!script?.length) {
return 0;
@ -55,8 +59,8 @@ export function setSchnorrSighashFlags(flags: bigint, witness: string[]): bigint
if (!witness?.length) {
return flags;
}
const hasAnnex = witness.length > 1 && witness[witness.length - 1].startsWith('50');
if (witness?.length === (hasAnnex ? 2 : 1)) {
const taprootInfo = parseTaproot(witness);
if (taprootInfo.keyPath) {
// keypath spend, signature is the only witness item
if (witness[0].length === 130) {
flags |= setSighashFlags(flags, witness[0]);
@ -64,12 +68,13 @@ export function setSchnorrSighashFlags(flags: bigint, witness: string[]): bigint
flags |= TransactionFlags.sighash_default;
}
} else {
// scriptpath spend, all items except for the script, control block and annex could be signatures
for (let i = 0; i < witness.length - (hasAnnex ? 3 : 2); i++) {
// scriptpath spend, all initial stack items could be signatures
const stack = taprootInfo.stack;
for (const w of stack) {
// handle probable signatures
if (witness[i].length === 130) {
flags |= setSighashFlags(flags, witness[i]);
} else if (witness[i].length === 128) {
if (w.length === 130) {
flags |= setSighashFlags(flags, w);
} else if (w.length === 128) {
flags |= TransactionFlags.sighash_default;
}
}
@ -287,14 +292,8 @@ export function processInputSignatures(vin: Vin): SigInfo[] {
signatures = extractDERSignaturesWitness(vin.witness || []);
break;
case 'v1_p2tr': {
const hasAnnex = vin.witness?.length > 1 && vin.witness[vin.witness.length - 1].startsWith('50');
const isKeyspend = vin.witness?.length === (hasAnnex ? 2 : 1);
if (isKeyspend) {
signatures = extractSchnorrSignatures(vin.witness);
} else {
const stackItems = vin.witness?.slice(0, hasAnnex ? -3 : -2);
signatures = extractSchnorrSignatures(stackItems);
}
const taprootInfo = parseTaproot(vin.witness);
signatures = extractSchnorrSignatures(taprootInfo.stack);
} break;
default:
// non-signed input types?
@ -406,12 +405,11 @@ export function fillUnsignedInput(vin: Vin): { missingSigs: number, bytes: numbe
}
}
break;
case 'v1_p2tr':
const hasAnnex = vin.witness?.length > 1 && vin.witness[vin.witness.length - 1].startsWith('50');
if (vin.inner_witnessscript_asm) {
const stackItems = vin.witness.slice(0, hasAnnex ? -3 : -2);
case 'v1_p2tr': {
const taprootInfo = parseTaproot(vin.witness);
signatures = extractSchnorrSignatures(taprootInfo.stack);
if (taprootInfo.scriptPath) {
if (/^OP_PUSHBYTES_32 [a-fA-F0-9]{64} OP_CHECKSIG$/.test(vin.inner_witnessscript_asm)) {
signatures = extractSchnorrSignatures(stackItems);
if (!signatures.length) {
missingSigs = 1;
bytes = 65;
@ -421,7 +419,6 @@ export function fillUnsignedInput(vin: Vin): { missingSigs: number, bytes: numbe
multisig = parseTapscriptMultisig(vin.inner_witnessscript_asm);
if (multisig) {
signatures = extractSchnorrSignatures(stackItems);
if (multisig.m - signatures.length > 0) {
missingSigs = multisig.m - signatures.length;
bytes = 65 * missingSigs + (multisig.n - multisig.m); // empty witness items for each non-signing keys
@ -429,9 +426,8 @@ export function fillUnsignedInput(vin: Vin): { missingSigs: number, bytes: numbe
}
}
let unanimousMultisig = parseTapscriptUnanimousMultisig(vin.inner_witnessscript_asm);
const unanimousMultisig = parseTapscriptUnanimousMultisig(vin.inner_witnessscript_asm);
if (unanimousMultisig) {
signatures = extractSchnorrSignatures(stackItems);
if (unanimousMultisig - signatures.length > 0) {
missingSigs = unanimousMultisig - signatures.length;
bytes = 65 * missingSigs;
@ -439,14 +435,13 @@ export function fillUnsignedInput(vin: Vin): { missingSigs: number, bytes: numbe
}
}
} else { // Assume keyspend
signatures = extractSchnorrSignatures(vin.witness?.slice(0, hasAnnex ? -1 : undefined) || []);
if (!signatures.length) {
missingSigs = 1;
bytes = 65;
addToWitness = true;
}
}
break;
} break;
default:
break;
}
@ -483,6 +478,11 @@ export function isNonStandard(tx: Transaction, height?: number, network?: string
return true;
}
// legacy sigops
if (isNonStandardLegacySigops(tx, height, network)) {
return true;
}
// input validation
for (const vin of tx.vin) {
if (vin.is_coinbase) {
@ -516,25 +516,24 @@ export function isNonStandard(tx: Transaction, height?: number, network?: string
}
// bad-witness-nonstandard
if (vin.prevout?.scriptpubkey_type === 'v1_p2tr' && vin.witness?.length) {
const hasAnnex = vin.witness.length > 1 && vin.witness[vin.witness.length - 1].startsWith('50');
const taprootInfo = parseTaproot(vin.witness);
// annex is non-standard
if (hasAnnex) {
if (taprootInfo.annex) {
return true;
}
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
if (taprootInfo.scriptPath) {
// script path spend
const controlBlock = vin.witness[vin.witness.length - (hasAnnex ? 2 : 1)];
// control block is required
if (!controlBlock.length) {
if (!taprootInfo.controlBlock.length) {
return false;
} else {
// Leaf version must be 0xc0 (aka Tapscript, see BIP 342)
if ((parseInt(controlBlock.slice(0, 2), 16) & 0xfe) !== 0xc0) {
if (taprootInfo.scriptPath.leafVersion !== 0xc0) {
return false;
}
}
// remaining witness items (except for the script) must be within MAX_STANDARD_TAPSCRIPT_STACK_ITEM_SIZE limit
if (vin.witness.slice(0, vin.witness.length - (hasAnnex ? 3 : 2)).some(v => v.length > 160)) {
if (taprootInfo.stack.some(v => v.length > 160)) {
return false;
}
}
@ -544,6 +543,7 @@ export function isNonStandard(tx: Transaction, height?: number, network?: string
// output validation
let opreturnCount = 0;
let opreturnBytes = 0;
for (const vout of tx.vout) {
// scriptpubkey
if (['nonstandard', 'provably_unspendable', 'empty'].includes(vout.scriptpubkey_type)) {
@ -567,10 +567,7 @@ export function isNonStandard(tx: Transaction, height?: number, network?: string
}
} else if (vout.scriptpubkey_type === 'op_return') {
opreturnCount++;
if ((vout.scriptpubkey.length / 2) > MAX_OP_RETURN_RELAY) {
// over default datacarrier limit
return true;
}
opreturnBytes += vout.scriptpubkey.length / 2;
}
// dust
// (we could probably hardcode this for the different output types...)
@ -592,9 +589,11 @@ export function isNonStandard(tx: Transaction, height?: number, network?: string
}
}
// multi-op-return
if (opreturnCount > 1) {
return true;
// op_return
if (opreturnCount > 0) {
if (!isStandardOpReturn(opreturnBytes, opreturnCount, height, network)) {
return true;
}
}
// TODO: non-mandatory-script-verify-flag
@ -668,6 +667,49 @@ function isStandardEphemeralDust(tx: Transaction, height?: number, network?: str
return false;
}
// OP_RETURN size & count limits were lifted in v28.3/v29.2/v30.0
const OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 108_000,
'testnet': 4_750_000,
'signet': 276_500,
'': 921_000,
};
const MAX_DATACARRIER_BYTES = 83;
function isStandardOpReturn(bytes: number, outputs: number,height?: number, network?: string): boolean {
if (
(height == null || (
OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height >= OP_RETURN_STANDARDNESS_ACTIVATION_HEIGHT[network]
)) // limits lifted
|| // OR
(bytes <= MAX_DATACARRIER_BYTES && outputs <= 1) // below old limits
) {
return true;
}
return false;
}
// New legacy sigops limit started to be enforced in v30.0
const LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT = {
'testnet4': 108_000,
'testnet': 4_750_000,
'signet': 276_500,
'': 921_000,
};
function isNonStandardLegacySigops(tx: Transaction, height?: number, network?: string): boolean {
if (
height == null || (
LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT[network]
&& height >= LEGACY_SIGOPS_STANDARDNESS_ACTIVATION_HEIGHT[network]
)
) {
if (!checkSigopsBIP54(tx, MAX_TX_LEGACY_SIGOPS)) {
return true;
}
}
return false;
}
// A witness program is any valid scriptpubkey that consists of a 1-byte push opcode
// followed by a data push between 2 and 40 bytes.
// https://github.com/bitcoin/bitcoin/blob/2c79abc7ad4850e9e3ba32a04c530155cda7f980/src/script/script.cpp#L224-L240
@ -805,10 +847,8 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
// created before taproot activation don't need to have any witness data
// (see https://mempool.space/tx/b10c007c60e14f9d087e0291d4d0c7869697c6681d979c6639dbd960792b4d41)
if (vin.witness?.length) {
// in taproot, if there are at least two witness elements, and the first byte of the last element is 0x50, that last element is an annex
const hasAnnex = vin.witness?.length > 1 && vin.witness?.[vin.witness.length - 1].startsWith('50');
// script spends have more than one witness item, not counting the annex (if present)
if (vin.witness.length > (hasAnnex ? 2 : 1)) {
const taprootInfo = parseTaproot(vin.witness);
if (taprootInfo.scriptPath) {
// the script itself is the second-to-last witness item, not counting the annex
const asm = vin.inner_witnessscript_asm;
// inscriptions smuggle data within an 'OP_0 OP_IF ... OP_ENDIF' envelope
@ -816,7 +856,7 @@ export function getTransactionFlags(tx: Transaction, cpfpInfo?: CpfpInfo, replac
flags |= TransactionFlags.inscription;
}
}
if (hasAnnex) {
if (taprootInfo.annex) {
flags |= TransactionFlags.annex;
}
}
@ -1653,6 +1693,41 @@ export function countSigops(transaction: Transaction): number {
return sigops;
}
/**
* see https://github.com/bitcoin/bitcoin/blob/25c45bb0d0bd6618ec9296a1a43605657124e5de/src/policy/policy.cpp#L166-L193
* returns true if the transactions is permitted under bip54 sigops rules
*
* "Unlike the existing block wide sigop limit which counts sigops present in the block
* itself (including the scriptPubKey which is not executed until spending later), BIP54
* counts sigops in the block where they are potentially executed (only).
* This means sigops in the spent scriptPubKey count toward the limit.
* `fAccurate` means correctly accounting sigops for CHECKMULTISIGs(VERIFY) with 16 pubkeys
* or fewer. This method of accounting was introduced by BIP16, and BIP54 reuses it.
* The GetSigOpCount call on the previous scriptPubKey counts both bare and P2SH sigops."
*/
function checkSigopsBIP54(tx: Transaction, limit: number = MAX_TX_LEGACY_SIGOPS): boolean {
let sigops = 0;
for (const input of tx.vin) {
if (input.scriptsig_asm) {
sigops += countScriptSigops(input.scriptsig_asm);
}
if (input.prevout) {
// P2SH redeem script
if (input.prevout.scriptpubkey_type === 'p2sh' && input.inner_redeemscript_asm) {
sigops += countScriptSigops(input.inner_redeemscript_asm);
} else {
// prevout scriptpubkey
sigops += countScriptSigops(input.prevout.scriptpubkey_asm);
}
}
if (sigops > limit) {
return false;
}
}
return true;
}
function scriptPubKeyToAddress(scriptPubKey: string, network: string): { address: string, type: string } {
// P2PKH
if (/^76a914[0-9a-f]{40}88ac$/.test(scriptPubKey)) {
@ -2157,3 +2232,73 @@ const opcodes = {
254: 'OP_PUBKEY',
255: 'OP_INVALIDOPCODE',
};
export interface ParsedTaproot {
keyPath: boolean;
scriptPath?: {
leafVersion: number;
parity: number;
script: string;
simplicityScript?: string; // liquid only
merkleBranches: string[];
internalKey: string;
isNUMS?: boolean;
}
stack: string[]; // witness items excluding annex, script, control block
annex?: string;
controlBlock?: string;
annexIndex?: number;
controlBlockIndex?: number;
scriptIndex?: number;
}
/**
* Parse out the different parts of a taproot spend from a p2tr witness
* if present, `witness` MUST be a valid p2tr witness stack, otherwise the result will be garbage
* otherwise assume this is an unsigned input from a non finalized PSBT
*/
export function parseTaproot(witness: string[]): ParsedTaproot | null {
if (!witness?.length) {
return { keyPath: true, stack: [] };
}
const parsed: ParsedTaproot = {
keyPath: true,
stack: witness.slice(0, 1), // assume keyspend for now
};
const hasAnnex = witness.length > 1 && witness[witness.length - 1].startsWith('50');
if (hasAnnex) {
parsed.annexIndex = witness.length - 1;
parsed.annex = witness[parsed.annexIndex];
}
if (witness.length > (hasAnnex ? 2 : 1)) {
parsed.keyPath = false;
parsed.controlBlockIndex = witness.length - (hasAnnex ? 2 : 1);
parsed.scriptIndex = witness.length - (hasAnnex ? 3 : 2);
// control block is the last non-annex element
parsed.controlBlock = witness[parsed.controlBlockIndex];
const leafVersionParity = parseInt(parsed.controlBlock.slice(0, 2), 16);
const internalKey = parsed.controlBlock.slice(2, 66);
parsed.scriptPath = {
// script is the second last non-annex element
script: witness[parsed.scriptIndex],
// first two bytes are the leaf version & parity bit
leafVersion: leafVersionParity & 0xfe,
parity: leafVersionParity & 0x01,
internalKey: internalKey,
isNUMS: internalKey === '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0',
merkleBranches: [],
};
for (let i = 66; (i + 64) <= parsed.controlBlock.length; i += 64) {
parsed.scriptPath.merkleBranches.push(parsed.controlBlock.slice(i, i + 64));
}
// remaining items are the initial stack
parsed.stack = witness.slice(0, (hasAnnex ? -3 : -2));
if (parsed.scriptPath.leafVersion === 0xbe) {
// override script stuff for simplicity
parsed.scriptIndex = 1;
parsed.scriptPath.simplicityScript = witness[parsed.scriptIndex];
}
}
return parsed;
}

View file

@ -1 +1 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny-ps" viewBox="0 0 241 241" xml:space="preserve"><title>Mempool Space K.K.</title><g><g><path fill="#2E3349" d="M241.37,211.23c0,16.56-13.43,29.99-29.99,29.99H30.36c-16.56,0-29.99-13.43-29.99-29.99V30.21 c0-16.56,13.43-29.99,29.99-29.99h181.02c16.56,0,29.99,13.43,29.99,29.99V211.23z"></path><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="120.8689" y1="68.6556" x2="120.8689" y2="301.1491"><stop offset="0" stop-color="#AE61FF"></stop><stop offset="1" stop-color="#13EFD8"></stop></linearGradient><path fill="url(#SVGID_1_)" d="M0.32,120.99v90.24c0,16.56,13.49,29.99,30.14,29.99h180.82c16.64,0,30.13-13.43,30.13-29.99 v-90.24H0.32z"></path></g><g><path fill="#FFFFFF" fill-opacity="0.3" d="M212.72,209c0,3.7-2.53,6.7-5.65,6.7h-31.24c-3.12,0-5.65-3-5.65-6.7V32.44c0-3.7,2.53-6.7,5.65-6.7h31.24 c3.12,0,5.65,3,5.65,6.7V209z"></path></g></g></svg>
<?xml version="1.0" encoding="utf-8" standalone="yes"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny-ps" viewBox="0 0 241 241" xml:space="preserve"><title>Mempool Holdings S.A. de C.V.</title><g><g><path fill="#2E3349" d="M241.37,211.23c0,16.56-13.43,29.99-29.99,29.99H30.36c-16.56,0-29.99-13.43-29.99-29.99V30.21 c0-16.56,13.43-29.99,29.99-29.99h181.02c16.56,0,29.99,13.43,29.99,29.99V211.23z"></path><linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="120.8689" y1="68.6556" x2="120.8689" y2="301.1491"><stop offset="0" stop-color="#AE61FF"></stop><stop offset="1" stop-color="#13EFD8"></stop></linearGradient><path fill="url(#SVGID_1_)" d="M0.32,120.99v90.24c0,16.56,13.49,29.99,30.14,29.99h180.82c16.64,0,30.13-13.43,30.13-29.99 v-90.24H0.32z"></path></g><g><path fill="#FFFFFF" fill-opacity="0.3" d="M212.72,209c0,3.7-2.53,6.7-5.65,6.7h-31.24c-3.12,0-5.65-3-5.65-6.7V32.44c0-3.7,2.53-6.7,5.65-6.7h31.24 c3.12,0,5.65,3,5.65,6.7V209z"></path></g></g></svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View file

@ -1,10 +1,10 @@
/* Theme */
$bg: #1d1f31;
$active-bg: #11131f;
$bg: #11131f;
$active-bg: #000000;
$hover-bg: #12131e;
$fg: #fff;
$nav-bg: $bg;
$title-fg: #4a68b9;
$title-fg: #2055e3;
$taproot: #eba814;
$taproot-light: #d5a90a;
@ -18,11 +18,11 @@ $gray-700: $fg;
$nav-tabs-link-active-bg: $active-bg;
$primary: #105fb0;
$secondary: #2d3348;
$tertiary: #653b9c;
$success: #1a9436;
$info: #1bd8f4;
$primary: #007cfa;
$secondary: #272f4e;
$tertiary: #6225b2;
$success: #0aab2f;
$info: #00ddff;
$h5-font-size: 1.15rem !default;
@ -79,13 +79,13 @@ $dropdown-link-active-bg: $active-bg;
--skeleton-bg: #2e324e;
--skeleton-bg-light: #5d6182;
--box-bg: #24273e;
--stat-box-bg: #181b2d;
--box-bg: #171c2a;
--stat-box-bg: #0b1018;
--alert-bg: #3a1c61;
--navbar-bg: #212121;
--transparent-fg: #ffffff66;
--fade-out-box-bg-start: rgba(36, 39, 62, 0);
--fade-out-box-bg-end: rgba(36, 39, 62, 1);
--transparent-fg: #ffffffbb;
--fade-out-box-bg-start: rgba(23, 28, 42, 0);
--fade-out-box-bg-end: rgba(23, 28, 42, 1);
--opacity: 0.57;
--testnet: #1d486f;
@ -102,12 +102,12 @@ $dropdown-link-active-bg: $active-bg;
--taproot-light: #d5a90a;
--taproot-dark: #9d7c05;
--green: #3bcc49;
--red: #dc3545;
--green: #83fd00;
--red: #ff3d00;
--yellow: #fff000;
--grey: #6c757d;
--grey: #7e7e7e;
--tooltip-grey: #b1b1b1;
--orange: #b86d12;
--orange: #ff9f00;
--search-button: #4d2d77;
--search-button-border: #472a6e;

View file

@ -1,10 +1,10 @@
/* Theme */
$bg: #11131f;
$active-bg: #000000;
$bg: #1d1f31;
$active-bg: #11131f;
$hover-bg: #12131e;
$fg: #fff;
$nav-bg: $bg;
$title-fg: #2055e3;
$title-fg: #4a68b9;
$taproot: #eba814;
$taproot-light: #d5a90a;
@ -18,11 +18,11 @@ $gray-700: $fg;
$nav-tabs-link-active-bg: $active-bg;
$primary: #007cfa;
$secondary: #272f4e;
$tertiary: #6225b2;
$success: #0aab2f;
$info: #00ddff;
$primary: #105fb0;
$secondary: #2d3348;
$tertiary: #653b9c;
$success: #1a9436;
$info: #1bd8f4;
$h5-font-size: 1.15rem !default;
@ -76,12 +76,12 @@ $dropdown-link-active-bg: $active-bg;
--skeleton-bg: #2e324e;
--skeleton-bg-light: #5d6182;
--box-bg: #171c2a;
--stat-box-bg: #0b1018;
--box-bg: #24273e;
--stat-box-bg: #181b2d;
--alert-bg: #3a1c61;
--transparent-fg: #ffffffbb;
--fade-out-box-bg-start: rgba(23, 28, 42, 0);
--fade-out-box-bg-end: rgba(23, 28, 42, 1);
--transparent-fg: #ffffff66;
--fade-out-box-bg-start: rgba(36, 39, 62, 0);
--fade-out-box-bg-end: rgba(36, 39, 62, 1);
--opacity: 0.57;
--testnet: #1d486f;
@ -98,12 +98,12 @@ $dropdown-link-active-bg: $active-bg;
--taproot-light: #d5a90a;
--taproot-dark: #9d7c05;
--green: #83fd00;
--red: #ff3d00;
--green: #3bcc49;
--red: #dc3545;
--yellow: #fff000;
--grey: #7e7e7e;
--grey: #6c757d;
--tooltip-grey: #b1b1b1;
--orange: #ff9f00;
--orange: #b86d12;
--search-button: #4d2d77;
--search-button-border: #472a6e;

View file

@ -64,6 +64,8 @@ bitcoin_checkconfig()
echo "Missing data directory: ${bitcoin_data_dir}"
exit 1
fi
touch "${bitcoin_data_dir}/debug.log"
chmod 644 "${bitcoin_data_dir}/debug.log"
chown -R "${bitcoin_user}:${bitcoin_group}" "${bitcoin_data_dir}"
if [ ! -f "${bitcoin_config_file}" ]