mirror of
https://github.com/mempool/mempool.git
synced 2025-03-26 17:51:45 +01:00
Liquid peg outs address indexing: support for duplicates and minor fixes
This commit is contained in:
parent
1e15428a63
commit
163c0b6d78
@ -154,7 +154,9 @@ class ElementsParser {
|
||||
|
||||
// First, get the current UTXOs that need to be scanned in the block
|
||||
const utxos = await this.$getFederationUtxosToScan(auditProgress.lastBlockAudit);
|
||||
// logger.debug(`Found ${utxos.length} Federation UTXOs to scan in Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip}`);
|
||||
|
||||
// Get the peg-out addresses that need to be scanned
|
||||
const redeemAddresses = await this.$getRedeemAddressesToScan();
|
||||
|
||||
// The fast way: check if these UTXOs are still unspent as of the current block with gettxout
|
||||
let spentAsTip: any[];
|
||||
@ -163,41 +165,33 @@ class ElementsParser {
|
||||
const utxosToParse = await this.$getFederationUtxosToParse(utxos);
|
||||
spentAsTip = utxosToParse.spentAsTip;
|
||||
unspentAsTip = utxosToParse.unspentAsTip;
|
||||
// logger.debug(`${unspentAsTip.length} / ${utxos.length} Federation UTXOs are unspent as of tip`);
|
||||
} else { // If the audit status is too far in the past, it is useless to look for still unspent txos since they will all be spent as of the tip
|
||||
logger.debug(`Found ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses to scan in Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip}`);
|
||||
logger.debug(`${unspentAsTip.length} / ${utxos.length} Federation UTXOs are unspent as of tip`);
|
||||
} else { // If the audit status is too far in the past, it is useless and wasteful to look for still unspent txos since they will all be spent as of the tip
|
||||
spentAsTip = utxos;
|
||||
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`);
|
||||
|
||||
// Logging during initial indexing
|
||||
if (auditProgress.confirmedTip - auditProgress.lastBlockAudit > 150) {
|
||||
// Logging
|
||||
const elapsedSeconds = (Date.now() / 1000) - timer;
|
||||
if (elapsedSeconds > 5) {
|
||||
const runningFor = (Date.now() / 1000) - startedAt;
|
||||
const blockPerSeconds = indexedThisRun / elapsedSeconds;
|
||||
indexingSpeeds.push(blockPerSeconds);
|
||||
if (indexingSpeeds.length > 100) indexingSpeeds.shift(); // Keep the length of the up to 100 last indexing speeds
|
||||
const eta = (auditProgress.confirmedTip - auditProgress.lastBlockAudit) / (indexingSpeeds.reduce((a, b) => a + b, 0) / indexingSpeeds.length);
|
||||
logger.debug(`Scanning ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses at height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip} | ~${blockPerSeconds.toFixed(2)} blocks/sec | elapsed: ${(runningFor / 60).toFixed(2)} minutes | ETA: ${(eta / 60).toFixed(2)} minutes`);
|
||||
const meanIndexingSpeed = indexingSpeeds.reduce((a, b) => a + b, 0) / indexingSpeeds.length;
|
||||
const eta = (auditProgress.confirmedTip - auditProgress.lastBlockAudit) / meanIndexingSpeed;
|
||||
logger.debug(`Scanning ${utxos.length} Federation UTXOs and ${redeemAddresses.length} Peg-Out Addresses at Bitcoin block height #${auditProgress.lastBlockAudit} / #${auditProgress.confirmedTip} | ~${meanIndexingSpeed.toFixed(2)} blocks/sec | elapsed: ${(runningFor / 60).toFixed(0)} minutes | ETA: ${(eta / 60).toFixed(0)} minutes`);
|
||||
timer = Date.now() / 1000;
|
||||
indexedThisRun = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 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`);
|
||||
|
||||
const blockHash: IBitcoinApi.ChainTips = await bitcoinSecondClient.getBlockHash(auditProgress.lastBlockAudit);
|
||||
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, redeemAddresses);
|
||||
await DB.query(`COMMIT;`);
|
||||
// logger.debug(`Watched for spending of ${nbUtxos} Federation UTXOs in block ${auditProgress.lastBlockAudit} / ${auditProgress.confirmedTip}`);
|
||||
|
||||
// Finally, update the lastblockupdate of the remaining UTXOs and save to the database
|
||||
const [minBlockUpdate] = await DB.query(`SELECT MIN(lastblockupdate) AS lastblockupdate FROM federation_txos WHERE unspent = 1`)
|
||||
@ -236,14 +230,15 @@ class ElementsParser {
|
||||
return {spentAsTip, unspentAsTip};
|
||||
}
|
||||
|
||||
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...
|
||||
protected async $parseBitcoinBlock(block: IBitcoinApi.Block, spentAsTip: any[], unspentAsTip: any[], confirmedTip: number, redeemAddressesData: any[] = []) {
|
||||
const redeemAddresses: string[] = redeemAddressesData.map(redeemAddress => redeemAddress.bitcoinaddress);
|
||||
for (const tx of block.tx) {
|
||||
let mightRedeemInThisTx = false; // If a Federation UTXO is spent in this block, we might find a peg-out address in the outputs...
|
||||
// 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;
|
||||
mightRedeemInThisTx = 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);
|
||||
@ -269,16 +264,31 @@ class ElementsParser {
|
||||
logger.debug(`Added new Federation UTXO ${tx.txid}:${output.n} (${output.value * 100000000} sats), change address: ${output.scriptPubKey.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}`);
|
||||
if (mightRedeemInThisTx && output.scriptPubKey.address && redeemAddresses.includes(output.scriptPubKey.address)) {
|
||||
// Find the number of times output.scriptPubKey.address appears in redeemAddresses. There can be address reuse for peg-outs...
|
||||
const matchingAddress: any[] = redeemAddressesData.filter(redeemAddress => redeemAddress.bitcoinaddress === output.scriptPubKey.address && -redeemAddress.amount === Math.round(output.value * 100000000));
|
||||
if (matchingAddress.length > 0) {
|
||||
if (matchingAddress.length > 1) {
|
||||
// If there are more than one peg out address with the same amount, we can't know which one redeemed the UTXO: we take the oldest one
|
||||
matchingAddress.sort((a, b) => a.datetime - b.datetime);
|
||||
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}, datetime ${matchingAddress[0].datetime}`);
|
||||
} else {
|
||||
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${matchingAddress[0].bitcoinaddress}, amount ${matchingAddress[0].amount}`);
|
||||
}
|
||||
const query_add_redeem = `UPDATE elements_pegs SET bitcointxid = ?, bitcoinindex = ? WHERE bitcoinaddress = ? AND amount = ? AND datetime = ?`;
|
||||
const params_add_redeem: (string | number)[] = [tx.txid, output.n, matchingAddress[0].bitcoinaddress, matchingAddress[0].amount, matchingAddress[0].datetime];
|
||||
await DB.query(query_add_redeem, params_add_redeem);
|
||||
const index = redeemAddressesData.indexOf(matchingAddress[0]);
|
||||
redeemAddressesData.splice(index, 1);
|
||||
redeemAddresses.splice(index, 1);
|
||||
} else { // The output amount does not match the peg-out amount... log it
|
||||
logger.debug(`Found redeem txid ${tx.txid}:${output.n} to peg-out address ${output.scriptPubKey.address} but output amount ${Math.round(output.value * 100000000)} does not match the peg-out amount!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const utxo of spentAsTip) {
|
||||
await DB.query(`UPDATE federation_txos SET lastblockupdate = ? WHERE txid = ? AND txindex = ?`, [block.height, utxo.txid, utxo.txindex]);
|
||||
}
|
||||
@ -318,10 +328,10 @@ class ElementsParser {
|
||||
return rows[0]['number'];
|
||||
}
|
||||
|
||||
protected async $getRedeemAddressesToScan(): Promise<string[]> {
|
||||
const query = `SELECT bitcoinaddress FROM elements_pegs where amount < 0 AND bitcoinaddress != '' AND bitcointxid = '';`;
|
||||
protected async $getRedeemAddressesToScan(): Promise<any[]> {
|
||||
const query = `SELECT datetime, amount, 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);
|
||||
return rows;
|
||||
}
|
||||
|
||||
///////////// DATA QUERY //////////////
|
||||
@ -423,9 +433,9 @@ class ElementsParser {
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
// Get the 300 most recent pegouts from the federation
|
||||
// Get recent pegouts from the federation (3 months old)
|
||||
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 query = `SELECT txid, txindex, amount, bitcoinaddress, bitcointxid, bitcoinindex, datetime AS blocktime FROM elements_pegs WHERE amount < 0 AND datetime > UNIX_TIMESTAMP(TIMESTAMPADD(DAY, -90, CURRENT_TIMESTAMP())) ORDER BY blocktime;`;
|
||||
const [rows] = await DB.query(query);
|
||||
return rows;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
<div [ngClass]="{'widget': widget, 'address-container': !widget}">
|
||||
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
<div [ngClass]="{'widget': widget}">
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
|
@ -1,10 +1,3 @@
|
||||
.address-container {
|
||||
@media (min-width: 1100px) {
|
||||
margin-left: 80px;
|
||||
margin-right: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-border {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
|
@ -3,7 +3,7 @@
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
</a>
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ federationAddresses.length }} <span i18n="shared.addresses">addresses</span></div>
|
||||
@ -19,7 +19,7 @@
|
||||
<div class="fee-estimation-container loading-container">
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit/wallet/addresses' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
<h5 class="card-title"><ng-container i18n="liquid.federation-wallet">Liquid Federation Wallet</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
</a>
|
||||
<div class="card-text">
|
||||
<div class="skeleton-loader"></div>
|
||||
|
@ -1,7 +1,5 @@
|
||||
<div [ngClass]="{'widget': widget}">
|
||||
|
||||
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
|
@ -5,8 +5,6 @@
|
||||
<h1 i18n="liquid.recent-pegs">Recent Peg-In / Out's</h1>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!widget && isLoading" class="spinner-border ml-3" role="status"></div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
@ -14,7 +12,8 @@
|
||||
<thead style="vertical-align: middle;">
|
||||
<th class="transaction text-left" [ngClass]="{'widget': widget}" i18n="shared.transaction">Transaction</th>
|
||||
<th class="amount text-right" [ngClass]="{'widget': widget}" i18n="shared.amount">Amount</th>
|
||||
<th class="output text-left" *ngIf="!widget" i18n="liquid.bitcoin-funding-redeem">BTC Funding / Redeem</th>
|
||||
<th class="output text-left" *ngIf="!widget" i18n="liquid.fund-redemption-tx">Fund / Redemption Tx</th>
|
||||
<th class="address text-left" *ngIf="!widget" i18n="liquid.bitcoin-address">BTC Address</th>
|
||||
<th class="timestamp text-right" i18n="shared.date" [ngClass]="{'widget': widget}">Date</th>
|
||||
</thead>
|
||||
<tbody *ngIf="recentPegs$ | async as pegs; else skeleton" [style]="isLoading ? 'opacity: 0.75' : ''">
|
||||
@ -23,17 +22,17 @@
|
||||
<td class="transaction text-left widget">
|
||||
<ng-container *ngIf="peg.amount > 0">
|
||||
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vin=' + peg.txindex">
|
||||
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
|
||||
<app-truncate [text]="peg.txid"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="peg.amount < 0">
|
||||
<a [routerLink]="['/tx' | relativeUrl, peg.txid]" [fragment]="'vout=' + peg.txindex">
|
||||
<app-truncate [text]="peg.txid" [lastChars]="6"></app-truncate>
|
||||
<app-truncate [text]="peg.txid"></app-truncate>
|
||||
</a>
|
||||
</ng-container>
|
||||
</td>
|
||||
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
|
||||
{{ peg.amount > 0 ? '+' : '-' }}<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
|
||||
<td class="amount text-right widget" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
|
||||
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
|
||||
</td>
|
||||
<td class="timestamp text-right widget">
|
||||
<app-time kind="since" [time]="peg.blocktime"></app-time>
|
||||
@ -55,7 +54,7 @@
|
||||
</ng-container>
|
||||
</td>
|
||||
<td class="amount text-right" [ngClass]="{'credit': peg.amount > 0, 'debit': peg.amount < 0}">
|
||||
{{ peg.amount > 0 ? '+' : '-' }}<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true"></app-amount>
|
||||
<app-amount [satoshis]="peg.amount" [noFiat]="true" [forceBtc]="true" [addPlus]="true"></app-amount>
|
||||
</td>
|
||||
<td class="output text-left">
|
||||
<ng-container *ngIf="peg.bitcointxid; else redeemInProgress">
|
||||
@ -65,7 +64,7 @@
|
||||
</ng-container>
|
||||
<ng-template #redeemInProgress>
|
||||
<ng-container *ngIf="peg.bitcoinaddress; else noRedeem">
|
||||
<span class="text-muted" i18n="liquid.redemption-in-progress">BTC Redemption in progress...</span>
|
||||
<i><span class="text-muted" i18n="liquid.redemption-in-progress">Peg out in progress...</span></i>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</td>
|
||||
|
@ -32,7 +32,7 @@ tr, td, th {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
max-width: 120px;
|
||||
}
|
||||
.transaction.widget {
|
||||
width: 40%;
|
||||
@ -62,7 +62,7 @@ tr, td, th {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
@media (max-width: 840px) {
|
||||
@media (max-width: 825px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -72,7 +72,7 @@ tr, td, th {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
@media (max-width: 527px) {
|
||||
@media (max-width: 840px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ import { WebsocketService } from '../../../services/websocket.service';
|
||||
})
|
||||
export class RecentPegsListComponent implements OnInit {
|
||||
@Input() widget: boolean = false;
|
||||
@Input() recentPegIns$: Observable<RecentPeg[]>;
|
||||
@Input() recentPegOuts$: Observable<RecentPeg[]>;
|
||||
@Input() recentPegIns$: Observable<RecentPeg[]> = of([]);
|
||||
@Input() recentPegOuts$: Observable<RecentPeg[]> = of([]);
|
||||
|
||||
env: Env;
|
||||
isLoading = true;
|
||||
@ -133,6 +133,7 @@ export class RecentPegsListComponent implements OnInit {
|
||||
return b.blocktime - a.blocktime;
|
||||
});
|
||||
}),
|
||||
filter(recentPegs => recentPegs.length > 0),
|
||||
tap(_ => this.isLoading = false),
|
||||
share()
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="fee-estimation-container">
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit/pegs' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
<h5 class="card-title"><ng-container i18n="liquid.recent-pegs">Recent Peg-In / Out's</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div class="card-text">
|
||||
<div class="fee-text">{{ (+currentPeg.amount) / 100000000 | number: '1.2-2' }} <span>L-BTC</span></div>
|
||||
<span class="fiat">
|
||||
<span>As of block <a [routerLink]="['/block', currentPeg.hash]" target="_blank">{{ currentPeg.lastBlockUpdate }}</a></span>
|
||||
<span>As of block <a [routerLink]="['/block', currentPeg.hash]">{{ currentPeg.lastBlockUpdate }}</a></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -276,7 +276,7 @@
|
||||
</div>
|
||||
<div class="item">
|
||||
<a class="title-link" [routerLink]="['/audit' | relativeUrl]">
|
||||
<h5 class="card-title"><ng-container i18n="dashboard.btc-reserves">BTC Reserves</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
<h5 class="card-title"><ng-container i18n="dashboard.btc-reserves">BTC Reserves</ng-container> <fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="font-size: 13px; color: #4a68b9"></fa-icon></h5>
|
||||
</a>
|
||||
<ng-container *ngIf="(currentReserves$ | async) as currentReserves; else loadingTransactions">
|
||||
<p i18n-ngbTooltip="liquid.last-bitcoin-audit-block" [ngbTooltip]="'BTC reserves last updated at Bitcoin block ' + (currentReserves.lastBlockUpdate)" placement="top" class="card-text">{{ +(currentReserves.amount) / 100000000 | number: '1.2-2' }} <span class="bitcoin-color">BTC</span></p>
|
||||
|
Loading…
x
Reference in New Issue
Block a user