Merge branch 'master' into natsoni/block-first-seen-audit

This commit is contained in:
natsoni 2024-10-13 17:41:56 +09:00
commit 198d79f149
No known key found for this signature in database
GPG key ID: C65917583181743B
27 changed files with 445 additions and 88 deletions

View file

@ -1,4 +1,4 @@
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi { export interface AbstractBitcoinApi {
@ -23,6 +23,7 @@ export interface AbstractBitcoinApi {
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>; $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
$sendRawTransaction(rawTransaction: string): Promise<string>; $sendRawTransaction(rawTransaction: string): Promise<string>;
$testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>; $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise<TestMempoolAcceptResult[]>;
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult>;
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>; $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
$getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>; $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
$getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>; $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;

View file

@ -218,3 +218,21 @@ export interface TestMempoolAcceptResult {
}, },
['reject-reason']?: string, ['reject-reason']?: string,
} }
export interface SubmitPackageResult {
package_msg: string;
"tx-results": { [wtxid: string]: TxResult };
"replaced-transactions"?: string[];
}
export interface TxResult {
txid: string;
"other-wtxid"?: string;
vsize?: number;
fees?: {
base: number;
"effective-feerate"?: number;
"effective-includes"?: string[];
};
error?: string;
}

View file

@ -1,6 +1,6 @@
import * as bitcoinjs from 'bitcoinjs-lib'; import * as bitcoinjs from 'bitcoinjs-lib';
import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory';
import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IBitcoinApi, SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks'; import blocks from '../blocks';
import mempool from '../mempool'; import mempool from '../mempool';
@ -196,6 +196,10 @@ class BitcoinApi implements AbstractBitcoinApi {
} }
} }
$submitPackage(rawTransactions: string[], maxfeerate?: number, maxburnamount?: number): Promise<SubmitPackageResult> {
return this.bitcoindClient.submitPackage(rawTransactions, maxfeerate ?? undefined, maxburnamount ?? undefined);
}
async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
return { return {

View file

@ -48,6 +48,8 @@ class BitcoinRoutes {
.post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion) .post(config.MEMPOOL.API_URL_PREFIX + 'psbt/addparents', this.postPsbtCompletion)
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from', this.getBlocksByBulk.bind(this))
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this)) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks-bulk/:from/:to', this.getBlocksByBulk.bind(this))
// Temporarily add txs/package endpoint for all backends until esplora supports it
.post(config.MEMPOOL.API_URL_PREFIX + 'txs/package', this.$submitPackage)
; ;
if (config.MEMPOOL.BACKEND !== 'esplora') { if (config.MEMPOOL.BACKEND !== 'esplora') {
@ -794,6 +796,19 @@ class BitcoinRoutes {
} }
} }
private async $submitPackage(req: Request, res: Response) {
try {
const rawTxs = Common.getTransactionsFromRequest(req);
const maxfeerate = parseFloat(req.query.maxfeerate as string);
const maxburnamount = parseFloat(req.query.maxburnamount as string);
const result = await bitcoinClient.submitPackage(rawTxs, maxfeerate ?? undefined, maxburnamount ?? undefined);
res.send(result);
} catch (e: any) {
handleError(req, res, 400, e.message && e.code ? 'submitpackage RPC error: ' + JSON.stringify({ code: e.code, message: e.message })
: (e.message || 'Error'));
}
}
} }
export default new BitcoinRoutes(); export default new BitcoinRoutes();

View file

@ -5,7 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact
import { IEsploraApi } from './esplora-api.interface'; import { IEsploraApi } from './esplora-api.interface';
import logger from '../../logger'; import logger from '../../logger';
import { Common } from '../common'; import { Common } from '../common';
import { TestMempoolAcceptResult } from './bitcoin-api.interface'; import { SubmitPackageResult, TestMempoolAcceptResult } from './bitcoin-api.interface';
interface FailoverHost { interface FailoverHost {
host: string, host: string,
@ -332,6 +332,10 @@ class ElectrsApi implements AbstractBitcoinApi {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
$submitPackage(rawTransactions: string[]): Promise<SubmitPackageResult> {
throw new Error('Method not implemented.');
}
$getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> { $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout); return this.failoverRouter.$get<IEsploraApi.Outspend>('/tx/' + txId + '/outspend/' + vout);
} }

View file

@ -83,6 +83,7 @@ module.exports = {
signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+ signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
stop: 'stop', stop: 'stop',
submitBlock: 'submitblock', // bitcoind v0.7.0+ submitBlock: 'submitblock', // bitcoind v0.7.0+
submitPackage: 'submitpackage',
validateAddress: 'validateaddress', validateAddress: 'validateaddress',
verifyChain: 'verifychain', // bitcoind v0.9.0+ verifyChain: 'verifychain', // bitcoind v0.9.0+
verifyMessage: 'verifymessage', verifyMessage: 'verifymessage',

View file

@ -525,7 +525,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID this.accelerationUUID,
costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;
@ -624,7 +625,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
cardTag, cardTag,
`accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`, `accelerator-${this.tx.txid.substring(0, 15)}-${Math.round(new Date().getTime() / 1000)}`,
this.accelerationUUID this.accelerationUUID,
costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;
@ -714,7 +716,8 @@ export class AccelerateCheckout implements OnInit, OnDestroy {
tokenResult.token, tokenResult.token,
tokenResult.details.cashAppPay.cashtag, tokenResult.details.cashAppPay.cashtag,
tokenResult.details.cashAppPay.referenceId, tokenResult.details.cashAppPay.referenceId,
this.accelerationUUID this.accelerationUUID,
costUSD
).subscribe({ ).subscribe({
next: () => { next: () => {
this.processing = false; this.processing = false;

View file

@ -9,7 +9,7 @@
<div class="interval"> <div class="interval">
<div class="interval-time"> <div class="interval-time">
@if (eta) { @if (eta) {
~<app-time [time]="eta?.wait / 1000"></app-time> <!-- <span *ngIf="accelerateRatio > 1" class="compare"> ({{ accelerateRatio }}x faster)</span> --> ~<app-time [time]="eta?.wait / 1000"></app-time>
} }
</div> </div>
</div> </div>
@ -48,8 +48,6 @@
<div class="interval-time"> <div class="interval-time">
<app-time [time]="acceleratedToMined"></app-time> <app-time [time]="acceleratedToMined"></app-time>
</div> </div>
} @else if (standardETA && !tx.status.confirmed) {
<!-- ~<app-time [time]="standardETA / 1000 - now"></app-time> -->
} }
</div> </div>
</div> </div>

View file

@ -11,19 +11,14 @@ import { MiningService } from '../../services/mining.service';
}) })
export class AccelerationTimelineComponent implements OnInit, OnChanges { export class AccelerationTimelineComponent implements OnInit, OnChanges {
@Input() transactionTime: number; @Input() transactionTime: number;
@Input() acceleratedAt: number;
@Input() tx: Transaction; @Input() tx: Transaction;
@Input() accelerationInfo: Acceleration; @Input() accelerationInfo: Acceleration;
@Input() eta: ETA; @Input() eta: ETA;
// A mined transaction has standard ETA and accelerated ETA undefined
// A transaction in mempool has either standardETA defined (if accelerated) or acceleratedETA defined (if not accelerated yet)
@Input() standardETA: number;
@Input() acceleratedETA: number;
acceleratedAt: number;
now: number; now: number;
accelerateRatio: number; accelerateRatio: number;
useAbsoluteTime: boolean = false; useAbsoluteTime: boolean = false;
interval: number;
firstSeenToAccelerated: number; firstSeenToAccelerated: number;
acceleratedToMined: number; acceleratedToMined: number;
@ -36,30 +31,17 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
this.acceleratedAt = this.tx.acceleratedAt ?? new Date().getTime() / 1000; this.updateTimes();
this.miningService.getPools().subscribe(pools => { this.miningService.getPools().subscribe(pools => {
for (const pool of pools) { for (const pool of pools) {
this.poolsData[pool.unique_id] = pool; this.poolsData[pool.unique_id] = pool;
} }
}); });
this.updateTimes();
this.interval = window.setInterval(this.updateTimes.bind(this), 60000);
} }
ngOnChanges(changes): void { ngOnChanges(changes): void {
// Hide standard ETA while we don't have a proper standard ETA calculation, see https://github.com/mempool/mempool/issues/65 this.updateTimes();
// if (changes?.eta?.currentValue || changes?.standardETA?.currentValue || changes?.acceleratedETA?.currentValue) {
// if (changes?.eta?.currentValue) {
// if (changes?.acceleratedETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.eta.currentValue.time / 1000) - this.now) / (Math.floor(changes.acceleratedETA.currentValue / 1000) - this.now));
// } else if (changes?.standardETA?.currentValue) {
// this.accelerateRatio = Math.floor((Math.floor(changes.standardETA.currentValue / 1000) - this.now) / (Math.floor(changes.eta.currentValue.time / 1000) - this.now));
// }
// }
// }
} }
updateTimes(): void { updateTimes(): void {
@ -68,10 +50,6 @@ export class AccelerationTimelineComponent implements OnInit, OnChanges {
this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime); this.firstSeenToAccelerated = Math.max(0, this.acceleratedAt - this.transactionTime);
this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt); this.acceleratedToMined = Math.max(0, this.tx.status.block_time - this.acceleratedAt);
} }
ngOnDestroy(): void {
clearInterval(this.interval);
}
onHover(event, status: string): void { onHover(event, status: string): void {
if (status === 'seen') { if (status === 'seen') {

View file

@ -27,6 +27,14 @@
<app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login> <app-twitter-login customClass="btn btn-sm" width="220px" redirectTo="/testnet4/faucet" buttonString="Sign up with Twitter"></app-twitter-login>
</div> </div>
} }
@else if (user && user.status === 'pending' && !user.email && user.snsId) {
<div class="alert alert-danger w-100 col d-flex justify-content-center text-left">
<span class="d-flex">
<fa-icon [icon]="['fas', 'exclamation-triangle']" [fixedWidth]="true" class="mr-1"></fa-icon>
<span>Please <a class="text-primary" [routerLink]="['/services/account/settings']">verify your account</a> by providing a valid email address. To mitigate spam, we delete unverified accounts at regular intervals.</span>
</span>
</div>
}
@else if (error === 'not_available') { @else if (error === 'not_available') {
<!-- User logged in but not a paid user or did not link its Twitter account --> <!-- User logged in but not a paid user or did not link its Twitter account -->
<div class="alert alert-mempool d-block text-center w-100"> <div class="alert alert-mempool d-block text-center w-100">

View file

@ -1,7 +1,6 @@
import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core"; import { Component, OnDestroy, OnInit, ChangeDetectorRef } from "@angular/core";
import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms"; import { FormBuilder, FormGroup, Validators, ValidatorFn, AbstractControl, ValidationErrors } from "@angular/forms";
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { StorageService } from "../../services/storage.service";
import { ServicesApiServices } from "../../services/services-api.service"; import { ServicesApiServices } from "../../services/services-api.service";
import { getRegex } from "../../shared/regex.utils"; import { getRegex } from "../../shared/regex.utils";
import { StateService } from "../../services/state.service"; import { StateService } from "../../services/state.service";
@ -34,7 +33,6 @@ export class FaucetComponent implements OnInit, OnDestroy {
constructor( constructor(
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private storageService: StorageService,
private servicesApiService: ServicesApiServices, private servicesApiService: ServicesApiServices,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private stateService: StateService, private stateService: StateService,
@ -56,14 +54,17 @@ export class FaucetComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
this.user = this.storageService.getAuth()?.user ?? null; this.servicesApiService.userSubject$.subscribe(user => {
if (!this.user) { this.user = user;
this.loading = false; if (!user) {
return; this.loading = false;
} this.cd.markForCheck();
return;
// Setup form }
this.updateFaucetStatus(); // Setup form
this.updateFaucetStatus();
this.cd.markForCheck();
});
// Track transaction // Track transaction
this.websocketService.want(['blocks', 'mempool-blocks']); this.websocketService.want(['blocks', 'mempool-blocks']);
@ -145,9 +146,6 @@ export class FaucetComponent implements OnInit, OnDestroy {
'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4')), this.getNotFaucetAddressValidator(faucetAddress)]], 'address': ['', [Validators.required, Validators.pattern(getRegex('address', 'testnet4')), this.getNotFaucetAddressValidator(faucetAddress)]],
'satoshis': [min, [Validators.required, Validators.min(min), Validators.max(max)]] 'satoshis': [min, [Validators.required, Validators.min(min), Validators.max(max)]]
}); });
this.loading = false;
this.cd.markForCheck();
} }
updateForm(min, max, faucetAddress: string): void { updateForm(min, max, faucetAddress: string): void {
@ -160,6 +158,8 @@ export class FaucetComponent implements OnInit, OnDestroy {
this.faucetForm.get('satoshis').updateValueAndValidity(); this.faucetForm.get('satoshis').updateValueAndValidity();
this.faucetForm.get('satoshis').markAsDirty(); this.faucetForm.get('satoshis').markAsDirty();
} }
this.loading = false;
this.cd.markForCheck();
} }
setAmount(value: number): void { setAmount(value: number): void {

View file

@ -9,7 +9,7 @@
@if (runestone?.etching.premine > 0) { @if (runestone?.etching.premine > 0) {
<ng-container i18n="ord.premine-n-runes"> <ng-container i18n="ord.premine-n-runes">
<span>Premine</span> <span>Premine</span>
<span class="amount"> {{ runestone.etching.premine >= 100000 ? (toNumber(runestone.etching.premine) | amountShortener:undefined:undefined:true) : runestone.etching.premine }} </span> <span class="amount"> {{ getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) >= 100000 ? (getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) | amountShortener:undefined:undefined:true) : getAmount(runestone.etching.premine, runestone.etching.divisibility || 0) }} </span>
{{ runestone.etching.symbol }} {{ runestone.etching.symbol }}
<span class="name">{{ runestone.etching.spacedName }}</span> <span class="name">{{ runestone.etching.spacedName }}</span>
<span> ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply)</span> <span> ({{ toNumber(runestone.etching.premine) / toNumber(runestone.etching.supply) * 100 | amountShortener:0}}% of total supply)</span>

View file

@ -9,4 +9,66 @@
<p class="red-color d-inline">{{ error }}</p> <a *ngIf="txId" [routerLink]="['/tx/' | relativeUrl, txId]">{{ txId }}</a> <p class="red-color d-inline">{{ error }}</p> <a *ngIf="txId" [routerLink]="['/tx/' | relativeUrl, txId]">{{ txId }}</a>
</form> </form>
@if (network === '' || network === 'testnet' || network === 'testnet4' || network === 'signet') {
<br>
<h1 class="text-left" style="margin-top: 1rem;" i18n="shared.submit-transactions|Submit Package">Submit Package</h1>
<form [formGroup]="submitTxsForm" (submit)="submitTxsForm.valid && submitTxs()" novalidate>
<div class="mb-3">
<textarea formControlName="txs" class="form-control" rows="5" i18n-placeholder="transaction.test-transactions" placeholder="Comma-separated list of raw transactions"></textarea>
</div>
<label i18n="test.tx.max-fee-rate">Maximum fee rate (sat/vB)</label>
<input type="number" class="form-control input-dark" formControlName="maxfeerate" id="maxfeerate"
[value]="10000" placeholder="10,000 s/vb" [class]="{invalid: invalidMaxfeerate}">
<label i18n="submitpackage.tx.max-burn-amount">Maximum burn amount (sats)</label>
<input type="number" class="form-control input-dark" formControlName="maxburnamount" id="maxburnamount"
[value]="0" placeholder="0 sat" [class]="{invalid: invalidMaxburnamount}">
<br>
<button [disabled]="isLoadingPackage" type="submit" class="btn btn-primary mr-2" i18n="shared.submit-transactions|Submit Package">Submit Package</button>
<p *ngIf="errorPackage" class="red-color d-inline">{{ errorPackage }}</p>
<p *ngIf="packageMessage" class="d-inline">{{ packageMessage }}</p>
</form>
<br>
<div class="box" *ngIf="results?.length">
<table class="accept-results table table-fixed table-borderless table-striped">
<tbody>
<tr>
<th class="allowed" i18n="test-tx.is-allowed">Allowed?</th>
<th class="txid" i18n="dashboard.latest-transactions.txid">TXID</th>
<th class="rate" i18n="transaction.effective-fee-rate|Effective transaction fee rate">Effective fee rate</th>
<th class="reason" i18n="test-tx.rejection-reason">Rejection reason</th>
</tr>
<ng-container *ngFor="let result of results;">
<tr>
<td class="allowed">
@if (result.error == null) {
<span></span>
}
@else {
<span></span>
}
</td>
<td class="txid">
@if (!result.error) {
<a [routerLink]="['/tx/' | relativeUrl, result.txid]"><app-truncate [text]="result.txid"></app-truncate></a>
} @else {
<app-truncate [text]="result.txid"></app-truncate>
}
</td>
<td class="rate">
<app-fee-rate *ngIf="result.fees?.['effective-feerate'] != null" [fee]="result.fees?.['effective-feerate'] * 100000"></app-fee-rate>
<span *ngIf="result.fees?.['effective-feerate'] == null">-</span>
</td>
<td class="reason">
{{ result.error || '-' }}
</td>
</tr>
</ng-container>
</tbody>
</table>
</div>
}
</div> </div>

View file

@ -7,6 +7,7 @@ import { OpenGraphService } from '../../services/opengraph.service';
import { seoDescriptionNetwork } from '../../shared/common.utils'; import { seoDescriptionNetwork } from '../../shared/common.utils';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
import { TxResult } from '../../interfaces/node-api.interface';
@Component({ @Component({
selector: 'app-push-transaction', selector: 'app-push-transaction',
@ -19,6 +20,16 @@ export class PushTransactionComponent implements OnInit {
txId: string = ''; txId: string = '';
isLoading = false; isLoading = false;
submitTxsForm: UntypedFormGroup;
errorPackage: string = '';
packageMessage: string = '';
results: TxResult[] = [];
invalidMaxfeerate = false;
invalidMaxburnamount = false;
isLoadingPackage = false;
network = this.stateService.network;
constructor( constructor(
private formBuilder: UntypedFormBuilder, private formBuilder: UntypedFormBuilder,
private apiService: ApiService, private apiService: ApiService,
@ -35,6 +46,14 @@ export class PushTransactionComponent implements OnInit {
txHash: ['', Validators.required], txHash: ['', Validators.required],
}); });
this.submitTxsForm = this.formBuilder.group({
txs: ['', Validators.required],
maxfeerate: ['', Validators.min(0)],
maxburnamount: ['', Validators.min(0)],
});
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.seoService.setTitle($localize`:@@meta.title.push-tx:Broadcast Transaction`); this.seoService.setTitle($localize`:@@meta.title.push-tx:Broadcast Transaction`);
this.seoService.setDescription($localize`:@@meta.description.push-tx:Broadcast a transaction to the ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} network using the transaction's hash.`); this.seoService.setDescription($localize`:@@meta.description.push-tx:Broadcast a transaction to the ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} network using the transaction's hash.`);
this.ogService.setManualOgImage('tx-push.jpg'); this.ogService.setManualOgImage('tx-push.jpg');
@ -59,7 +78,7 @@ export class PushTransactionComponent implements OnInit {
}, },
(error) => { (error) => {
if (typeof error.error === 'string') { if (typeof error.error === 'string') {
const matchText = error.error.match('"message":"(.*?)"'); const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"');
this.error = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error); this.error = 'Failed to broadcast transaction, reason: ' + (matchText && matchText[1] || error.error);
} else if (error.message) { } else if (error.message) {
this.error = 'Failed to broadcast transaction, reason: ' + error.message; this.error = 'Failed to broadcast transaction, reason: ' + error.message;
@ -70,6 +89,67 @@ export class PushTransactionComponent implements OnInit {
}); });
} }
submitTxs() {
let txs: string[] = [];
try {
txs = (this.submitTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim());
if (txs?.length === 1) {
this.pushTxForm.get('txHash').setValue(txs[0]);
this.submitTxsForm.get('txs').setValue('');
this.postTx();
return;
}
} catch (e) {
this.errorPackage = e?.message;
return;
}
let maxfeerate;
let maxburnamount;
this.invalidMaxfeerate = false;
this.invalidMaxburnamount = false;
try {
const maxfeerateVal = this.submitTxsForm.get('maxfeerate')?.value;
if (maxfeerateVal != null && maxfeerateVal !== '') {
maxfeerate = parseFloat(maxfeerateVal) / 100_000;
}
} catch (e) {
this.invalidMaxfeerate = true;
}
try {
const maxburnamountVal = this.submitTxsForm.get('maxburnamount')?.value;
if (maxburnamountVal != null && maxburnamountVal !== '') {
maxburnamount = parseInt(maxburnamountVal) / 100_000_000;
}
} catch (e) {
this.invalidMaxburnamount = true;
}
this.isLoadingPackage = true;
this.errorPackage = '';
this.results = [];
this.apiService.submitPackage$(txs, maxfeerate === 0.1 ? null : maxfeerate, maxburnamount === 0 ? null : maxburnamount)
.subscribe((result) => {
this.isLoadingPackage = false;
this.packageMessage = result['package_msg'];
for (let wtxid in result['tx-results']) {
this.results.push(result['tx-results'][wtxid]);
}
this.submitTxsForm.reset();
},
(error) => {
if (typeof error.error?.error === 'string') {
const matchText = error.error.error.replace(/\\/g, '').match('"message":"(.*?)"');
this.errorPackage = matchText && matchText[1] || error.error.error;
} else if (error.message) {
this.errorPackage = error.message;
}
this.isLoadingPackage = false;
});
}
private async handleColdcardPushTx(fragmentParams: URLSearchParams): Promise<boolean> { private async handleColdcardPushTx(fragmentParams: URLSearchParams): Promise<boolean> {
// maybe conforms to Coldcard nfc-pushtx spec // maybe conforms to Coldcard nfc-pushtx spec
if (fragmentParams && fragmentParams.get('t')) { if (fragmentParams && fragmentParams.get('t')) {

View file

@ -74,7 +74,7 @@ export class TestTransactionsComponent implements OnInit {
}, },
(error) => { (error) => {
if (typeof error.error === 'string') { if (typeof error.error === 'string') {
const matchText = error.error.match('"message":"(.*?)"'); const matchText = error.error.replace(/\\/g, '').match('"message":"(.*?)"');
this.error = matchText && matchText[1] || error.error; this.error = matchText && matchText[1] || error.error;
} else if (error.message) { } else if (error.message) {
this.error = error.message; this.error = error.message;

View file

@ -164,12 +164,12 @@
<br> <br>
</ng-container> </ng-container>
<ng-container *ngIf="transactionTime && isAcceleration"> <ng-container *ngIf="transactionTime > 0 && tx.acceleratedAt > 0 && isAcceleration">
<div class="title float-left"> <div class="title float-left">
<h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2> <h2 id="acceleration-timeline" i18n="transaction.acceleration-timeline|Acceleration Timeline">Acceleration Timeline</h2>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
<app-acceleration-timeline [transactionTime]="transactionTime" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)" [standardETA]="(standardETA$ | async)?.time"></app-acceleration-timeline> <app-acceleration-timeline [transactionTime]="transactionTime" [acceleratedAt]="tx.acceleratedAt" [tx]="tx" [accelerationInfo]="accelerationInfo" [eta]="(ETA$ | async)"></app-acceleration-timeline>
<br> <br>
</ng-container> </ng-container>

View file

@ -119,7 +119,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
txChanged$ = new BehaviorSubject<boolean>(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself) txChanged$ = new BehaviorSubject<boolean>(false); // triggered whenever this.tx changes (long term, we should refactor to make this.tx an observable itself)
isAccelerated$ = new BehaviorSubject<boolean>(false); // refactor this to make isAccelerated an observable itself isAccelerated$ = new BehaviorSubject<boolean>(false); // refactor this to make isAccelerated an observable itself
ETA$: Observable<ETA | null>; ETA$: Observable<ETA | null>;
standardETA$: Observable<ETA | null>;
isCached: boolean = false; isCached: boolean = false;
now = Date.now(); now = Date.now();
da$: Observable<DifficultyAdjustment>; da$: Observable<DifficultyAdjustment>;
@ -883,21 +882,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.miningStats = stats; this.miningStats = stats;
this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable this.isAccelerated$.next(this.isAcceleration); // hack to trigger recalculation of ETA without adding another source observable
}); });
if (!this.tx.status?.confirmed) {
this.standardETA$ = combineLatest([
this.stateService.mempoolBlocks$.pipe(startWith(null)),
this.stateService.difficultyAdjustment$.pipe(startWith(null)),
]).pipe(
map(([mempoolBlocks, da]) => {
return this.etaService.calculateUnacceleratedETA(
this.tx,
mempoolBlocks,
da,
this.cpfpInfo,
);
})
)
}
} }
this.isAccelerated$.next(this.isAcceleration); this.isAccelerated$.next(this.isAcceleration);
} }

View file

@ -452,4 +452,22 @@ export interface TestMempoolAcceptResult {
"effective-includes": string[], "effective-includes": string[],
}, },
['reject-reason']?: string, ['reject-reason']?: string,
} }
export interface SubmitPackageResult {
package_msg: string;
"tx-results": { [wtxid: string]: TxResult };
"replaced-transactions"?: string[];
}
export interface TxResult {
txid: string;
"other-wtxid"?: string;
vsize?: number;
fees?: {
base: number;
"effective-feerate"?: number;
"effective-includes"?: string[];
};
error?: string;
}

View file

@ -1,7 +1,8 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights,
RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult } from '../interfaces/node-api.interface'; RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult,
SubmitPackageResult} from '../interfaces/node-api.interface';
import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs'; import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs';
import { StateService } from './state.service'; import { StateService } from './state.service';
import { Transaction } from '../interfaces/electrs.interface'; import { Transaction } from '../interfaces/electrs.interface';
@ -244,6 +245,19 @@ export class ApiService {
return this.httpClient.post<TestMempoolAcceptResult[]>(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, rawTxs); return this.httpClient.post<TestMempoolAcceptResult[]>(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, rawTxs);
} }
submitPackage$(rawTxs: string[], maxfeerate?: number, maxburnamount?: number): Observable<SubmitPackageResult> {
const queryParams = [];
if (maxfeerate) {
queryParams.push(`maxfeerate=${maxfeerate}`);
}
if (maxburnamount) {
queryParams.push(`maxburnamount=${maxburnamount}`);
}
return this.httpClient.post<SubmitPackageResult>(this.apiBaseUrl + this.apiBasePath + '/api/v1/txs/package' + (queryParams.length > 0 ? `?${queryParams.join('&')}` : ''), rawTxs);
}
getTransactionStatus$(txid: string): Observable<any> { getTransactionStatus$(txid: string): Observable<any> {
return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status'); return this.httpClient.get<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status');
} }

View file

@ -135,16 +135,16 @@ export class ServicesApiServices {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate`, { txInput: txInput, userBid: userBid, accelerationUUID: accelerationUUID });
} }
accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string) { accelerateWithCashApp$(txInput: string, token: string, cashtag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/cashapp`, { txInput: txInput, token: token, cashtag: cashtag, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
} }
accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string) { accelerateWithApplePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/applePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
} }
accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string) { accelerateWithGooglePay$(txInput: string, token: string, cardTag: string, referenceId: string, accelerationUUID: string, userApprovedUSD: number) {
return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID }); return this.httpClient.post<any>(`${this.stateService.env.SERVICES_API}/accelerator/accelerate/googlePay`, { txInput: txInput, cardTag: cardTag, token: token, referenceId: referenceId, accelerationUUID: accelerationUUID, userApprovedUSD: userApprovedUSD });
} }
getAccelerations$(): Observable<Acceleration[]> { getAccelerations$(): Observable<Acceleration[]> {

View file

@ -70,6 +70,12 @@ export class GeolocationComponent implements OnChanges {
if (this.type === 'node') { if (this.type === 'node') {
const city = this.data.city ? this.data.city : ''; const city = this.data.city ? this.data.city : '';
// Handle city-states like Singapore or Hong Kong
if (city && city === this.data?.country) {
this.formattedLocation = `${this.data.country} ${getFlagEmoji(this.data.iso)}`;
return;
}
// City // City
this.formattedLocation = `${city}`; this.formattedLocation = `${city}`;

View file

@ -1,3 +1,19 @@
/*
MIT License
Copyright (c) 2024 HAUS HOPPE
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
*/
// Adapted from https://github.com/ordpool-space/ordpool-parser/tree/ce04d7a5b6bb1cf37b9fdadd77ba430f5bd6e7d6/src // Adapted from https://github.com/ordpool-space/ordpool-parser/tree/ce04d7a5b6bb1cf37b9fdadd77ba430f5bd6e7d6/src
// Utils functions to decode ord inscriptions // Utils functions to decode ord inscriptions

View file

@ -313,20 +313,24 @@ export function getRegex(type: RegexType, network?: Network): RegExp {
} }
regex += `)`; // End the non-capturing group regex += `)`; // End the non-capturing group
break; break;
// Match a date in the format YYYY-MM-DD (optional: HH:MM) // Match a date in the format YYYY-MM-DD (optional: HH:MM or HH:MM:SS)
// [Testing Order]: any order is fine // [Testing Order]: any order is fine
case `date`: case `date`:
regex += `(?:`; // Start a non-capturing group regex += `(?:`; // Start a non-capturing group
regex += `${NUMBER_CHARS}{4}`; // Exactly 4 digits regex += `${NUMBER_CHARS}{4}`; // Exactly 4 digits
regex += `[-/]`; // 1 instance of the symbol "-" or "/" regex += `[-/]`; // 1 instance of the symbol "-" or "/"
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits
regex += `[-/]`; // 1 instance of the symbol "-" or "/" regex += `[-/]`; // 1 instance of the symbol "-" or "/"
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits
regex += `(?:`; // Start a non-capturing group regex += `(?:`; // Start a non-capturing group
regex += ` `; // 1 instance of the symbol " " regex += ` `; // 1 instance of the symbol " "
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits
regex += `:`; // 1 instance of the symbol ":" regex += `:`; // 1 instance of the symbol ":"
regex += `${NUMBER_CHARS}{1,2}`; // Exactly 4 digits regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits
regex += `(?:`; // Start a non-capturing group for optional seconds
regex += `:`; // 1 instance of the symbol ":"
regex += `${NUMBER_CHARS}{1,2}`; // 1 or 2 digits
regex += `)?`; // End the non-capturing group
regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times regex += `)?`; // End the non-capturing group. This group appears 0 or 1 times
regex += `)`; // End the non-capturing group regex += `)`; // End the non-capturing group
break; break;

View file

@ -1,4 +1,5 @@
@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1 @reboot sleep 5 ; /usr/local/bin/bitcoind -testnet >/dev/null 2>&1
@reboot sleep 5 ; /usr/local/bin/bitcoind -testnet4 >/dev/null 2>&1
@reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1 @reboot sleep 5 ; /usr/local/bin/bitcoind -signet >/dev/null 2>&1
@reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/start mainnet @reboot sleep 10 ; screen -dmS mainnet /bitcoin/electrs/start mainnet
@reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/start testnet @reboot sleep 10 ; screen -dmS testnet /bitcoin/electrs/start testnet

View file

@ -47,6 +47,7 @@ UNFURL_INSTALL=ON
BITCOIN_MAINNET_ENABLE=ON BITCOIN_MAINNET_ENABLE=ON
BITCOIN_MAINNET_MINFEE_ENABLE=ON BITCOIN_MAINNET_MINFEE_ENABLE=ON
BITCOIN_TESTNET_ENABLE=ON BITCOIN_TESTNET_ENABLE=ON
BITCOIN_TESTNET4_ENABLE=ON
BITCOIN_SIGNET_ENABLE=ON BITCOIN_SIGNET_ENABLE=ON
BITCOIN_MAINNET_LIGHTNING_ENABLE=ON BITCOIN_MAINNET_LIGHTNING_ENABLE=ON
BITCOIN_TESTNET_LIGHTNING_ENABLE=ON BITCOIN_TESTNET_LIGHTNING_ENABLE=ON
@ -100,6 +101,13 @@ BITCOIN_TESTNET_P2P_PORT=18333
BITCOIN_TESTNET_RPC_HOST=127.0.0.1 BITCOIN_TESTNET_RPC_HOST=127.0.0.1
BITCOIN_TESTNET_RPC_PORT=18332 BITCOIN_TESTNET_RPC_PORT=18332
# used for firewall configuration
BITCOIN_TESTNET4_P2P_HOST=127.0.0.1
BITCOIN_TESTNET4_P2P_PORT=48333
# used for RPC communication
BITCOIN_TESTNET4_RPC_HOST=127.0.0.1
BITCOIN_TESTNET4_RPC_PORT=48332
# used for firewall configuration # used for firewall configuration
BITCOIN_SIGNET_P2P_HOST=127.0.0.1 BITCOIN_SIGNET_P2P_HOST=127.0.0.1
BITCOIN_SIGNET_P2P_PORT=18333 BITCOIN_SIGNET_P2P_PORT=18333
@ -139,6 +147,11 @@ ELECTRS_LIQUID_HTTP_PORT=3001
ELECTRS_TESTNET_HTTP_HOST=127.0.0.1 ELECTRS_TESTNET_HTTP_HOST=127.0.0.1
ELECTRS_TESTNET_HTTP_PORT=3002 ELECTRS_TESTNET_HTTP_PORT=3002
# set either socket or TCP host/port, not both
#ELECTRS_TESTNET4_HTTP_SOCK=/tmp/bitcoin.testnet4.electrs
ELECTRS_TESTNET4_HTTP_HOST=127.0.0.1
ELECTRS_TESTNET4_HTTP_PORT=3005
# set either socket or TCP host/port, not both # set either socket or TCP host/port, not both
#ELECTRS_SIGNET_HTTP_SOCK=/tmp/bitcoin.testnet.electrs #ELECTRS_SIGNET_HTTP_SOCK=/tmp/bitcoin.testnet.electrs
ELECTRS_SIGNET_HTTP_HOST=127.0.0.1 ELECTRS_SIGNET_HTTP_HOST=127.0.0.1
@ -164,6 +177,11 @@ MEMPOOL_LIQUID_HTTP_PORT=8998
MEMPOOL_TESTNET_HTTP_HOST=127.0.0.1 MEMPOOL_TESTNET_HTTP_HOST=127.0.0.1
MEMPOOL_TESTNET_HTTP_PORT=8997 MEMPOOL_TESTNET_HTTP_PORT=8997
# set either socket or TCP host/port, not both
#MEMPOOL_TESTNET4_HTTP_SOCK=/tmp/bitcoin.testnet.mempool
MEMPOOL_TESTNET4_HTTP_HOST=127.0.0.1
MEMPOOL_TESTNET4_HTTP_PORT=8990
# set either socket or TCP host/port, not both # set either socket or TCP host/port, not both
#MEMPOOL_BISQ_HTTP_SOCK=/tmp/bitcoin.bisq.mempool #MEMPOOL_BISQ_HTTP_SOCK=/tmp/bitcoin.bisq.mempool
MEMPOOL_BISQ_HTTP_HOST=127.0.0.1 MEMPOOL_BISQ_HTTP_HOST=127.0.0.1
@ -231,6 +249,7 @@ MYSQL_GROUP=mysql
# mempool mysql user/password # mempool mysql user/password
MEMPOOL_MAINNET_USER='mempool' MEMPOOL_MAINNET_USER='mempool'
MEMPOOL_TESTNET_USER='mempool_testnet' MEMPOOL_TESTNET_USER='mempool_testnet'
MEMPOOL_TESTNET4_USER='mempool_testnet4'
MEMPOOL_SIGNET_USER='mempool_signet' MEMPOOL_SIGNET_USER='mempool_signet'
MEMPOOL_MAINNET_LIGHTNING_USER='mempool_mainnet_lightning' MEMPOOL_MAINNET_LIGHTNING_USER='mempool_mainnet_lightning'
MEMPOOL_TESTNET_LIGHTNING_USER='mempool_testnet_lightning' MEMPOOL_TESTNET_LIGHTNING_USER='mempool_testnet_lightning'
@ -241,6 +260,7 @@ MEMPOOL_BISQ_USER='mempool_bisq'
# generate random hex string # generate random hex string
MEMPOOL_MAINNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_MAINNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
MEMPOOL_TESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_TESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
MEMPOOL_TESTNET4_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
MEMPOOL_SIGNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_SIGNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
MEMPOOL_MAINNET_LIGHTNING_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_MAINNET_LIGHTNING_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
MEMPOOL_TESTNET_LIGHTNING_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') MEMPOOL_TESTNET_LIGHTNING_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
@ -265,7 +285,9 @@ BITCOIN_HOME=/bitcoin
# bitcoin testnet data # bitcoin testnet data
BITCOIN_TESTNET_DATA=${BITCOIN_HOME}/testnet3 BITCOIN_TESTNET_DATA=${BITCOIN_HOME}/testnet3
# bitcoin testnet data # bitcoin testnet4 data
BITCOIN_TESTNET4_DATA=${BITCOIN_HOME}/testnet4
# bitcoin signet data
BITCOIN_SIGNET_DATA=${BITCOIN_HOME}/signet BITCOIN_SIGNET_DATA=${BITCOIN_HOME}/signet
# bitcoin electrs source/binaries # bitcoin electrs source/binaries
@ -279,6 +301,9 @@ ELECTRS_MAINNET_DATA=${ELECTRS_DATA_ROOT}/mainnet
# bitcoin testnet electrs database, only a few GB # bitcoin testnet electrs database, only a few GB
ELECTRS_TESTNET_ZPOOL=${ZPOOL} ELECTRS_TESTNET_ZPOOL=${ZPOOL}
ELECTRS_TESTNET_DATA=${ELECTRS_DATA_ROOT}/testnet ELECTRS_TESTNET_DATA=${ELECTRS_DATA_ROOT}/testnet
# bitcoin testnet4 electrs database, only a few GB
ELECTRS_TESTNET4_ZPOOL=${ZPOOL}
ELECTRS_TESTNET4_DATA=${ELECTRS_DATA_ROOT}/testnet4
# bitcoin signet electrs database, only a few GB # bitcoin signet electrs database, only a few GB
ELECTRS_SIGNET_ZPOOL=${ZPOOL} ELECTRS_SIGNET_ZPOOL=${ZPOOL}
ELECTRS_SIGNET_DATA=${ELECTRS_DATA_ROOT}/signet ELECTRS_SIGNET_DATA=${ELECTRS_DATA_ROOT}/signet
@ -332,7 +357,7 @@ BITCOIN_REPO_URL=https://github.com/bitcoin/bitcoin
BITCOIN_REPO_NAME=bitcoin BITCOIN_REPO_NAME=bitcoin
BITCOIN_REPO_BRANCH=master BITCOIN_REPO_BRANCH=master
#BITCOIN_LATEST_RELEASE=$(curl -s https://api.github.com/repos/bitcoin/bitcoin/releases/latest|grep tag_name|head -1|cut -d '"' -f4) #BITCOIN_LATEST_RELEASE=$(curl -s https://api.github.com/repos/bitcoin/bitcoin/releases/latest|grep tag_name|head -1|cut -d '"' -f4)
BITCOIN_LATEST_RELEASE=v25.1 BITCOIN_LATEST_RELEASE=v28.0rc2
echo -n '.' echo -n '.'
BISQ_REPO_URL=https://github.com/bisq-network/bisq BISQ_REPO_URL=https://github.com/bisq-network/bisq
@ -567,6 +592,15 @@ zfsCreateFilesystems()
done done
fi fi
# Bitcoin Testnet4
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
zfs create -o "mountpoint=${BITCOIN_TESTNET4_DATA}" "${ZPOOL}/bitcoin/testnet4"
for folder in chainstate indexes blocks
do
zfs create -o "mountpoint=${BITCOIN_TESTNET4_DATA}/${folder}" "${ZPOOL}/bitcoin/testnet4/${folder}"
done
fi
# Bitcoin Signet # Bitcoin Signet
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
zfs create -o "mountpoint=${BITCOIN_SIGNET_DATA}" "${ZPOOL}/bitcoin/signet" zfs create -o "mountpoint=${BITCOIN_SIGNET_DATA}" "${ZPOOL}/bitcoin/signet"
@ -594,6 +628,15 @@ zfsCreateFilesystems()
done done
fi fi
# electrs testnet4 data
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
zfs create -o "mountpoint=${ELECTRS_TESTNET4_DATA}" "${ELECTRS_TESTNET4_ZPOOL}/electrs/testnet4"
for folder in cache history txstore
do
zfs create -o "mountpoint=${ELECTRS_TESTNET4_DATA}/newindex/${folder}" "${ELECTRS_TESTNET4_ZPOOL}/electrs/testnet4/${folder}"
done
fi
# electrs signet data # electrs signet data
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
zfs create -o "mountpoint=${ELECTRS_SIGNET_DATA}" "${ELECTRS_SIGNET_ZPOOL}/electrs/signet" zfs create -o "mountpoint=${ELECTRS_SIGNET_DATA}" "${ELECTRS_SIGNET_ZPOOL}/electrs/signet"
@ -651,6 +694,15 @@ ext4CreateDir()
done done
fi fi
# Bitcoin Testnet4
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
mkdir -p "${BITCOIN_TESTNET4_DATA}"
for folder in chainstate indexes blocks
do
mkdir -p "${BITCOIN_TESTNET4_DATA}/${folder}"
done
fi
# Bitcoin Signet # Bitcoin Signet
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
mkdir -p "${BITCOIN_SIGNET_DATA}" mkdir -p "${BITCOIN_SIGNET_DATA}"
@ -678,6 +730,15 @@ ext4CreateDir()
done done
fi fi
# electrs testnet4 data
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
mkdir -p "${ELECTRS_TESTNET4_DATA}"
for folder in cache history txstore
do
mkdir -p "${ELECTRS_TESTNET4_DATA}/newindex/${folder}"
done
fi
# electrs signet data # electrs signet data
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
mkdir -p "${ELECTRS_SIGNET_DATA}" mkdir -p "${ELECTRS_SIGNET_DATA}"
@ -769,6 +830,7 @@ LN-Mainnet:Enable Bitcoin Mainnet Lightning:ON
LN-Testnet:Enable Bitcoin Testnet Lightning:ON LN-Testnet:Enable Bitcoin Testnet Lightning:ON
LN-Signet:Enable Bitcoin Signet Lightning:ON LN-Signet:Enable Bitcoin Signet Lightning:ON
Testnet:Enable Bitcoin Testnet:ON Testnet:Enable Bitcoin Testnet:ON
Testnet4:Enable Bitcoin Testnet4:ON
Signet:Enable Bitcoin Signet:ON Signet:Enable Bitcoin Signet:ON
Liquid:Enable Elements Liquid:ON Liquid:Enable Elements Liquid:ON
Liquidtestnet:Enable Elements Liquidtestnet:ON Liquidtestnet:Enable Elements Liquidtestnet:ON
@ -818,13 +880,19 @@ else
BITCOIN_TESTNET_ENABLE=OFF BITCOIN_TESTNET_ENABLE=OFF
fi fi
if grep Testnet4 $tempfile >/dev/null 2>&1;then
BITCOIN_TESTNET4_ENABLE=ON
else
BITCOIN_TESTNET4_ENABLE=OFF
fi
if grep Signet $tempfile >/dev/null 2>&1;then if grep Signet $tempfile >/dev/null 2>&1;then
BITCOIN_SIGNET_ENABLE=ON BITCOIN_SIGNET_ENABLE=ON
else else
BITCOIN_SIGNET_ENABLE=OFF BITCOIN_SIGNET_ENABLE=OFF
fi fi
if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_MAINNET_MINFEE_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_MAINNET_MINFEE_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_TESTNET4_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then
BITCOIN_INSTALL=ON BITCOIN_INSTALL=ON
else else
BITCOIN_INSTALL=OFF BITCOIN_INSTALL=OFF
@ -872,7 +940,7 @@ else
CLN_INSTALL=OFF CLN_INSTALL=OFF
fi fi
if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_TESTNET4_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then
BITCOIN_ELECTRS_INSTALL=ON BITCOIN_ELECTRS_INSTALL=ON
else else
BITCOIN_ELECTRS_INSTALL=OFF BITCOIN_ELECTRS_INSTALL=OFF
@ -1216,6 +1284,9 @@ if [ "${BITCOIN_ELECTRS_INSTALL}" = ON ];then
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}" osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}"
fi fi
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET4_DATA}"
fi
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}" osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}"
fi fi
@ -1520,7 +1591,7 @@ fi
# Bitcoin instance for Mainnet Minfee # # Bitcoin instance for Mainnet Minfee #
####################################### #######################################
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then if [ "${BITCOIN_MAINNET_MINFEE_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Minfee service" echo "[*] Installing Bitcoin Minfee service"
case $OS in case $OS in
@ -1550,6 +1621,23 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
esac esac
fi fi
#################################
# Bitcoin instance for Testnet4 #
#################################
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
echo "[*] Installing Bitcoin Testnet service"
case $OS in
FreeBSD)
;;
Debian)
osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/linux/bitcoin-testnet4.service" "${DEBIAN_SERVICE_HOME}"
;;
esac
fi
############################### ###############################
# Bitcoin instance for Signet # # Bitcoin instance for Signet #
############################### ###############################
@ -1616,6 +1704,14 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
echo "[*] FIXME: must only crontab enabled daemons" echo "[*] FIXME: must only crontab enabled daemons"
fi fi
#########################################
# Electrs instance for Bitcoin Testnet4 #
#########################################
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
echo "[*] FIXME: must only crontab enabled daemons"
fi
####################################### #######################################
# Electrs instance for Bitcoin Signet # # Electrs instance for Bitcoin Signet #
####################################### #######################################
@ -1668,11 +1764,15 @@ case $OS in
echo "[*] Installing Electrs Testnet Cronjob" echo "[*] Installing Electrs Testnet Cronjob"
crontab_bitcoin+="@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/start testnet\n" crontab_bitcoin+="@reboot sleep 70 ; screen -dmS testnet /bitcoin/electrs/start testnet\n"
fi fi
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
echo "[*] Installing Electrs Testnet4 Cronjob"
crontab_bitcoin+="@reboot sleep 110 ; screen -dmS testnet4 /bitcoin/electrs/start testnet4\n"
fi
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
echo "[*] Installing Electrs Signet Cronjob" echo "[*] Installing Electrs Signet Cronjob"
crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/start signet\n" crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/start signet\n"
fi fi
if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_TESTNET4_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then
echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" - echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" -
fi fi
@ -1700,7 +1800,7 @@ fi
##### Mempool -> Bitcoin Mainnet instance ##### Mempool -> Bitcoin Mainnet instance
if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_TESTNET4_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then
echo "[*] Creating Mempool instance for Bitcoin Mainnet" echo "[*] Creating Mempool instance for Bitcoin Mainnet"
osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/mainnet" osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/mainnet"
@ -1727,6 +1827,15 @@ if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/testnet && git checkout ${MEMPOOL_LATEST_RELEASE}" osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/testnet && git checkout ${MEMPOOL_LATEST_RELEASE}"
fi fi
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
echo "[*] Creating Mempool instance for Bitcoin Testnet4"
osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false
osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/testnet4"
echo "[*] Checking out Mempool ${MEMPOOL_LATEST_RELEASE} for Bitcoin Testnet4"
osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME}/testnet4 && git checkout ${MEMPOOL_LATEST_RELEASE}"
fi
if [ "${BITCOIN_TESTNET_LIGHTNING_ENABLE}" = ON ];then if [ "${BITCOIN_TESTNET_LIGHTNING_ENABLE}" = ON ];then
echo "[*] Creating Mempool instance for Lightning Network on Bitcoin Testnet" echo "[*] Creating Mempool instance for Lightning Network on Bitcoin Testnet"
osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false
@ -1804,6 +1913,9 @@ grant all on mempool.* to '${MEMPOOL_MAINNET_USER}'@'localhost' identified by '$
create database mempool_testnet; create database mempool_testnet;
grant all on mempool_testnet.* to '${MEMPOOL_TESTNET_USER}'@'localhost' identified by '${MEMPOOL_TESTNET_PASS}'; grant all on mempool_testnet.* to '${MEMPOOL_TESTNET_USER}'@'localhost' identified by '${MEMPOOL_TESTNET_PASS}';
create database mempool_testnet4;
grant all on mempool_testnet4.* to '${MEMPOOL_TESTNET4_USER}'@'localhost' identified by '${MEMPOOL_TESTNET4_PASS}';
create database mempool_signet; create database mempool_signet;
grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}'; grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}';
@ -1832,6 +1944,8 @@ declare -x MEMPOOL_MAINNET_USER="${MEMPOOL_MAINNET_USER}"
declare -x MEMPOOL_MAINNET_PASS="${MEMPOOL_MAINNET_PASS}" declare -x MEMPOOL_MAINNET_PASS="${MEMPOOL_MAINNET_PASS}"
declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}" declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}"
declare -x MEMPOOL_TESTNET_PASS="${MEMPOOL_TESTNET_PASS}" declare -x MEMPOOL_TESTNET_PASS="${MEMPOOL_TESTNET_PASS}"
declare -x MEMPOOL_TESTNET4_USER="${MEMPOOL_TESTNET4_USER}"
declare -x MEMPOOL_TESTNET4_PASS="${MEMPOOL_TESTNET4_PASS}"
declare -x MEMPOOL_SIGNET_USER="${MEMPOOL_SIGNET_USER}" declare -x MEMPOOL_SIGNET_USER="${MEMPOOL_SIGNET_USER}"
declare -x MEMPOOL_SIGNET_PASS="${MEMPOOL_SIGNET_PASS}" declare -x MEMPOOL_SIGNET_PASS="${MEMPOOL_SIGNET_PASS}"
declare -x MEMPOOL_MAINNET_LIGHTNING_USER="${MEMPOOL_MAINNET_LIGHTNING_USER}" declare -x MEMPOOL_MAINNET_LIGHTNING_USER="${MEMPOOL_MAINNET_LIGHTNING_USER}"
@ -1932,6 +2046,9 @@ EOF
if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable bitcoin-testnet.service osSudo "${ROOT_USER}" systemctl enable bitcoin-testnet.service
fi fi
if [ "${BITCOIN_TESTNET4_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable bitcoin-testnet4.service
fi
if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then
osSudo "${ROOT_USER}" systemctl enable bitcoin-signet.service osSudo "${ROOT_USER}" systemctl enable bitcoin-signet.service
fi fi

View file

@ -0,0 +1,22 @@
[Unit]
Description=Bitcoind-testnet4
After=network.target
[Service]
ExecStart=/usr/local/bin/bitcoind -conf=bitcoin.conf -daemon -testnet4 -printtoconsole -pid=/bitcoin/bitcoind-testnet4.pid
ExecStop=/usr/local/bin/bitcoin-cli -testnet4 stop
Type=forking
PIDFile=/bitcoin/bitcoind-testnet4.pid
Restart=on-failure
User=bitcoin
Group=bitcoin
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
[Install]
WantedBy=multi-user.target

View file

@ -10,6 +10,9 @@
"MEMPOOL_WEBSITE_URL": "https://mempool.space", "MEMPOOL_WEBSITE_URL": "https://mempool.space",
"LIQUID_WEBSITE_URL": "https://liquid.network", "LIQUID_WEBSITE_URL": "https://liquid.network",
"BISQ_WEBSITE_URL": "https://bisq.markets", "BISQ_WEBSITE_URL": "https://bisq.markets",
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 773911,
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 2417829,
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 127609,
"ITEMS_PER_PAGE": 25, "ITEMS_PER_PAGE": 25,
"LIGHTNING": true, "LIGHTNING": true,
"ACCELERATOR": true, "ACCELERATOR": true,