Liquid: Add support for peg outs display

This commit is contained in:
natsee 2024-01-30 11:11:30 +01:00
parent 639fc3dd5f
commit 73e6045549
No known key found for this signature in database
GPG Key ID: 233CF3150A89BED8
7 changed files with 83 additions and 8 deletions

View File

@ -163,6 +163,10 @@ class ElementsParser {
unspentAsTip = [];
}
// Get the peg-out addresses that need to be scanned
const redeemAddresses = await this.$getRedeemAddressesToScan();
if (redeemAddresses.length > 0) logger.debug(`Found ${redeemAddresses.length} peg-out addresses to scan`);
// The slow way: parse the block to look for the spending tx
logger.debug(`${spentAsTip.length} / ${utxos.length} Federation UTXOs are spent as of tip`);
@ -170,7 +174,7 @@ class ElementsParser {
const block: IBitcoinApi.Block = await bitcoinSecondClient.getBlock(blockHash, 2);
const nbUtxos = spentAsTip.length;
await DB.query('START TRANSACTION;');
await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip);
await this.$parseBitcoinBlock(block, spentAsTip, unspentAsTip, auditProgress.confirmedTip, redeemAddresses);
await DB.query(`COMMIT;`);
logger.debug(`Watched for spending of ${nbUtxos} Federation UTXOs in block ${auditProgress.lastBlockAudit} / ${auditProgress.confirmedTip}`);
@ -210,12 +214,14 @@ class ElementsParser {
return {spentAsTip, unspentAsTip};
}
protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number) {
protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddresses: string[] = []) {
let mightRedeemInThisBlock = false; // If a Federation UTXO is spent in this block, we might find a peg-out address in the outputs...
for (const tx of block.tx) {
// Check if the Federation UTXOs that was spent as of tip are spent in this block
for (const input of tx.vin) {
const txo = spentAsTip.find(txo => txo.txid === input.txid && txo.txindex === input.vout);
if (txo) {
mightRedeemInThisBlock = true;
await DB.query(`UPDATE federation_txos SET unspent = 0, lastblockupdate = ?, lasttimeupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, block.time, txo.txid, txo.txindex]);
// Remove the TXO from the utxo array
spentAsTip.splice(spentAsTip.indexOf(txo), 1);
@ -241,6 +247,13 @@ class ElementsParser {
logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} of ${output.value * 100000000} sats belonging to ${output.scriptPubKey.address} (Federation change address).`);
}
}
if (mightRedeemInThisBlock && output.scriptPubKey.address && redeemAddresses.includes(output.scriptPubKey.address)) {
const query_add_redeem = `UPDATE elements_pegs SET bitcointxid = ?, bitcoinindex = ? WHERE bitcoinaddress = ?`;
const params_add_redeem: (string | number)[] = [tx.txid, output.n, output.scriptPubKey.address];
await DB.query(query_add_redeem, params_add_redeem);
redeemAddresses.splice(redeemAddresses.indexOf(output.scriptPubKey.address), 1);
logger.debug(`Added redeem txid ${tx.txid}:${output.n} to peg-out address ${output.scriptPubKey.address}`);
}
}
}
@ -283,9 +296,15 @@ class ElementsParser {
return rows[0]['number'];
}
///////////// DATA QUERY //////////////
protected async $getRedeemAddressesToScan(): Promise<string[]> {
const query = `SELECT bitcoinaddress FROM elements_pegs where amount < 0 AND bitcoinaddress != '' AND bitcointxid = '';`;
const [rows]: any[] = await DB.query(query);
return rows.map((row: any) => row.bitcoinaddress);
}
public async $getAuditStatus(): Promise<any> {
///////////// DATA QUERY //////////////
public async $getAuditStatus(): Promise<any> {
const lastBlockAudit = await this.$getLastBlockAudit();
const bitcoinBlocksToSync = await this.$getBitcoinBlockchainState();
return {
@ -382,6 +401,12 @@ class ElementsParser {
return rows[0];
}
// Get the 300 most recent pegouts from the federation
public async $getRecentPegouts(): Promise<any> {
const query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs WHERE amount < 0 ORDER BY blocktime DESC LIMIT 300;`;
const [rows] = await DB.query(query);
return rows;
}
}
export default new ElementsParser();

View File

@ -19,6 +19,7 @@ class LiquidRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', this.$getElementsPegsByMonth)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves', this.$getFederationReserves)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/month', this.$getFederationReservesByMonth)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegouts', this.$getPegOuts)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses', this.$getFederationAddresses)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/addresses/previous-month', this.$getFederationAddressesOneMonthAgo)
.get(config.MEMPOOL.API_URL_PREFIX + 'liquid/reserves/utxos', this.$getFederationUtxos)
@ -176,6 +177,18 @@ class LiquidRoutes {
}
}
private async $getPegOuts(req: Request, res: Response) {
try {
const recentPegOuts = await elementsParser.$getRecentPegouts();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 30).toUTCString());
res.json(recentPegOuts);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
}
export default new LiquidRoutes();

View File

@ -15,6 +15,7 @@ import { WebsocketService } from '../../../services/websocket.service';
export class RecentPegsListComponent implements OnInit {
@Input() widget: boolean = false;
@Input() recentPegIns$: Observable<RecentPeg[]>;
@Input() recentPegOuts$: Observable<RecentPeg[]>;
env: Env;
isLoading = true;
@ -103,6 +104,7 @@ export class RecentPegsListComponent implements OnInit {
txid: utxo.pegtxid,
txindex: utxo.pegindex,
amount: utxo.amount,
bitcoinaddress: utxo.bitcoinaddress,
bitcointxid: utxo.txid,
bitcoinindex: utxo.txindex,
blocktime: utxo.pegblocktime,
@ -110,9 +112,30 @@ export class RecentPegsListComponent implements OnInit {
})),
share()
);
this.recentPegOuts$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.recentPegOuts$()),
share()
);
}
this.recentPegs$ = this.recentPegIns$;
this.recentPegs$ = combineLatest([
this.recentPegIns$,
this.recentPegOuts$
]).pipe(
map(([recentPegIns, recentPegOuts]) => {
return [
...recentPegIns,
...recentPegOuts
].sort((a, b) => {
return b.blocktime - a.blocktime;
});
}),
share()
);
}
ngOnDestroy(): void {

View File

@ -26,7 +26,7 @@
<div class="card">
<div class="card-body">
<app-recent-pegs-stats></app-recent-pegs-stats>
<app-recent-pegs-list [recentPegIns$]="recentPegIns$" [widget]="true"></app-recent-pegs-list>
<app-recent-pegs-list [recentPegIns$]="recentPegIns$" [recentPegOuts$]="recentPegOuts$"[widget]="true"></app-recent-pegs-list>
</div>
</div>
</div>

View File

@ -19,6 +19,7 @@ export class ReservesAuditDashboardComponent implements OnInit {
currentReserves$: Observable<CurrentPegs>;
federationUtxos$: Observable<FederationUtxo[]>;
recentPegIns$: Observable<RecentPeg[]>;
recentPegOuts$: Observable<RecentPeg[]>;
federationAddresses$: Observable<FederationAddress[]>;
federationAddressesOneMonthAgo$: Observable<any>;
liquidPegsMonth$: Observable<any>;
@ -110,6 +111,7 @@ export class ReservesAuditDashboardComponent implements OnInit {
txid: utxo.pegtxid,
txindex: utxo.pegindex,
amount: utxo.amount,
bitcoinaddress: utxo.bitcoinaddress,
bitcointxid: utxo.txid,
bitcoinindex: utxo.txindex,
blocktime: utxo.pegblocktime,
@ -118,6 +120,13 @@ export class ReservesAuditDashboardComponent implements OnInit {
share()
);
this.recentPegOuts$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),
switchMap(_ => this.apiService.recentPegOuts$()),
share()
);
this.federationAddresses$ = this.auditUpdated$.pipe(
filter(auditUpdated => auditUpdated === true),
throttleTime(40000),

View File

@ -101,8 +101,9 @@ export interface FederationUtxo {
export interface RecentPeg {
txid: string;
txindex: number; // input #0 for peg-ins
txindex: number;
amount: number;
bitcoinaddress: string;
bitcointxid: string;
bitcoinindex: number;
blocktime: number;

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';
import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators,
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo } from '../interfaces/node-api.interface';
PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg } from '../interfaces/node-api.interface';
import { BehaviorSubject, Observable, catchError, filter, of, shareReplay, take, tap } from 'rxjs';
import { StateService } from './state.service';
import { IBackendInfo, WebsocketResponse } from '../interfaces/websocket.interface';
@ -206,6 +206,10 @@ export class ApiService {
return this.httpClient.get<FederationUtxo[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/utxos');
}
recentPegOuts$(): Observable<RecentPeg[]> {
return this.httpClient.get<RecentPeg[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/pegouts');
}
federationAddressesOneMonthAgo$(): Observable<any> {
return this.httpClient.get<FederationAddress[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/liquid/reserves/addresses/previous-month');
}