Merge pull request #1643 from mempool/simon/lazy-load-inputs
Lazy load tx inputs in Bitcoin Core mode
This commit is contained in:
commit
3a6f64b2e3
@ -2,7 +2,7 @@ import { IEsploraApi } from './esplora-api.interface';
|
|||||||
|
|
||||||
export interface AbstractBitcoinApi {
|
export interface AbstractBitcoinApi {
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||||
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
|
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
|
||||||
$getBlockHeightTip(): Promise<number>;
|
$getBlockHeightTip(): Promise<number>;
|
||||||
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||||
$getBlockHash(height: number): Promise<string>;
|
$getBlockHash(height: number): Promise<string>;
|
||||||
|
@ -31,7 +31,8 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
|
|
||||||
|
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
|
||||||
// If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
|
// If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
|
||||||
const txInMempool = mempool.getMempool()[txId];
|
const txInMempool = mempool.getMempool()[txId];
|
||||||
if (txInMempool && addPrevout) {
|
if (txInMempool && addPrevout) {
|
||||||
@ -46,7 +47,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
});
|
});
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
return this.$convertTransaction(transaction, addPrevout);
|
return this.$convertTransaction(transaction, addPrevout, lazyPrevouts);
|
||||||
})
|
})
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
if (e.message.startsWith('The genesis block coinbase')) {
|
if (e.message.startsWith('The genesis block coinbase')) {
|
||||||
@ -126,7 +127,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
const outSpends: IEsploraApi.Outspend[] = [];
|
const outSpends: IEsploraApi.Outspend[] = [];
|
||||||
const tx = await this.$getRawTransaction(txId, true, false);
|
const tx = await this.$getRawTransaction(txId, true, false);
|
||||||
for (let i = 0; i < tx.vout.length; i++) {
|
for (let i = 0; i < tx.vout.length; i++) {
|
||||||
if (tx.status && tx.status.block_height == 0) {
|
if (tx.status && tx.status.block_height === 0) {
|
||||||
outSpends.push({
|
outSpends.push({
|
||||||
spent: false
|
spent: false
|
||||||
});
|
});
|
||||||
@ -145,7 +146,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
|
protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
|
||||||
let esploraTransaction: IEsploraApi.Transaction = {
|
let esploraTransaction: IEsploraApi.Transaction = {
|
||||||
txid: transaction.txid,
|
txid: transaction.txid,
|
||||||
version: transaction.version,
|
version: transaction.version,
|
||||||
@ -192,7 +193,7 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (addPrevout) {
|
if (addPrevout) {
|
||||||
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction);
|
esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts);
|
||||||
} else if (!transaction.confirmations) {
|
} else if (!transaction.confirmations) {
|
||||||
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
|
esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
|
||||||
}
|
}
|
||||||
@ -268,20 +269,30 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
return this.bitcoindClient.getRawMemPool(true);
|
return this.bitcoindClient.getRawMemPool(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction): Promise<IEsploraApi.Transaction> {
|
|
||||||
|
private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise<IEsploraApi.Transaction> {
|
||||||
if (transaction.vin[0].is_coinbase) {
|
if (transaction.vin[0].is_coinbase) {
|
||||||
transaction.fee = 0;
|
transaction.fee = 0;
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
let totalIn = 0;
|
let totalIn = 0;
|
||||||
for (const vin of transaction.vin) {
|
|
||||||
const innerTx = await this.$getRawTransaction(vin.txid, false, false);
|
for (let i = 0; i < transaction.vin.length; i++) {
|
||||||
vin.prevout = innerTx.vout[vin.vout];
|
if (lazyPrevouts && i > 12) {
|
||||||
this.addInnerScriptsToVin(vin);
|
transaction.vin[i].lazy = true;
|
||||||
totalIn += innerTx.vout[vin.vout].value;
|
continue;
|
||||||
|
}
|
||||||
|
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
|
||||||
|
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
|
||||||
|
this.addInnerScriptsToVin(transaction.vin[i]);
|
||||||
|
totalIn += innerTx.vout[transaction.vin[i].vout].value;
|
||||||
|
}
|
||||||
|
if (lazyPrevouts && transaction.vin.length > 12) {
|
||||||
|
transaction.fee = -1;
|
||||||
|
} else {
|
||||||
|
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
|
||||||
|
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
|
||||||
}
|
}
|
||||||
const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
|
|
||||||
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
|
|
||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,6 +33,8 @@ export namespace IEsploraApi {
|
|||||||
// Elements
|
// Elements
|
||||||
is_pegin?: boolean;
|
is_pegin?: boolean;
|
||||||
issuance?: Issuance;
|
issuance?: Issuance;
|
||||||
|
// Custom
|
||||||
|
lazy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Issuance {
|
interface Issuance {
|
||||||
|
@ -21,8 +21,8 @@ class TransactionUtils {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $getTransactionExtended(txId: string, addPrevouts = false): Promise<TransactionExtended> {
|
public async $getTransactionExtended(txId: string, addPrevouts = false, lazyPrevouts = false): Promise<TransactionExtended> {
|
||||||
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts);
|
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts, lazyPrevouts);
|
||||||
return this.extendTransaction(transaction);
|
return this.extendTransaction(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -778,9 +778,9 @@ class Routes {
|
|||||||
const endIndex = Math.min(startingIndex + 10, txIds.length);
|
const endIndex = Math.min(startingIndex + 10, txIds.length);
|
||||||
for (let i = startingIndex; i < endIndex; i++) {
|
for (let i = startingIndex; i < endIndex; i++) {
|
||||||
try {
|
try {
|
||||||
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true);
|
const transaction = await transactionUtils.$getTransactionExtended(txIds[i], true, true);
|
||||||
transactions.push(transaction);
|
transactions.push(transaction);
|
||||||
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i + 1) / endIndex * 100);
|
loadingIndicators.setProgress('blocktxs-' + req.params.hash, (i - startingIndex + 1) / (endIndex - startingIndex) * 100);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e));
|
logger.debug('getBlockTransactions error: ' + (e instanceof Error ? e.message : e));
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
let transactionObservable$: Observable<Transaction>;
|
let transactionObservable$: Observable<Transaction>;
|
||||||
if (history.state.data) {
|
if (history.state.data && history.state.data.fee !== -1) {
|
||||||
transactionObservable$ = of(history.state.data);
|
transactionObservable$ = of(history.state.data);
|
||||||
} else {
|
} else {
|
||||||
transactionObservable$ = this.electrsApiService
|
transactionObservable$ = this.electrsApiService
|
||||||
|
@ -60,7 +60,10 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchDefault>
|
<ng-container *ngSwitchDefault>
|
||||||
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
||||||
<span>{{ vin.issuance ? 'Issuance' : 'UNKNOWN' }}</span>
|
<span *ngIf="vin.lazy; else defaultNoPrevout" class="skeleton-loader"></span>
|
||||||
|
<ng-template #defaultNoPrevout>
|
||||||
|
<span>{{ vin.issuance ? 'Issuance' : 'UNKNOWN' }}</span>
|
||||||
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #defaultAddress>
|
<ng-template #defaultAddress>
|
||||||
<a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
|
<a [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey_address]" title="{{ vin.prevout.scriptpubkey_address }}">
|
||||||
@ -87,6 +90,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ng-template #defaultOutput>
|
<ng-template #defaultOutput>
|
||||||
|
<span *ngIf="vin.lazy" class="skeleton-loader"></span>
|
||||||
<app-amount *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount>
|
<app-amount *ngIf="vin.prevout" [satoshis]="vin.prevout.value"></app-amount>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</td>
|
</td>
|
||||||
@ -141,7 +145,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
|
<tr *ngIf="tx.vin.length > 12 && tx['@vinLimit']">
|
||||||
<td colspan="3" class="text-center">
|
<td colspan="3" class="text-center">
|
||||||
<button class="btn btn-sm btn-primary mt-2" (click)="tx['@vinLimit'] = false;"><span i18n="show-all">Show all</span> ({{ tx.vin.length - 10 }})</button>
|
<button class="btn btn-sm btn-primary mt-2" (click)="loadMoreInputs(tx);"><span i18n="show-all">Show all</span> ({{ tx.vin.length - 10 }})</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -261,9 +265,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="summary">
|
<div class="summary">
|
||||||
<div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase">
|
<div class="float-left mt-2-5" *ngIf="!transactionPage && !tx.vin[0].is_coinbase && tx.fee !== -1">
|
||||||
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block"> – {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span>
|
{{ tx.fee / (tx.weight / 4) | feeRounding }} <span class="symbol" i18n="shared.sat-vbyte|sat/vB">sat/vB</span> <span class="d-none d-sm-inline-block"> – {{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span> <span class="fiat"><app-fiat [value]="tx.fee"></app-fiat></span></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="float-left mt-2-5 grey-info-text" *ngIf="tx.fee === -1" i18n="transactions-list.load-to-reveal-fee-info">Show all inputs to reveal fee data</div>
|
||||||
|
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
<ng-container *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
|
<ng-container *ngIf="showConfirmations && latestBlock$ | async as latestBlock">
|
||||||
|
@ -139,4 +139,10 @@ h2 {
|
|||||||
|
|
||||||
.addr-right {
|
.addr-right {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grey-info-text {
|
||||||
|
color:#6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
@ -175,6 +175,17 @@ export class TransactionsListComponent implements OnInit, OnChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadMoreInputs(tx: Transaction) {
|
||||||
|
tx['@vinLimit'] = false;
|
||||||
|
|
||||||
|
this.electrsApiService.getTransaction$(tx.txid)
|
||||||
|
.subscribe((newTx) => {
|
||||||
|
tx.vin = newTx.vin;
|
||||||
|
tx.fee = newTx.fee;
|
||||||
|
this.ref.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
this.outspendsSubscription.unsubscribe();
|
this.outspendsSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,8 @@ export interface Vin {
|
|||||||
// Elements
|
// Elements
|
||||||
is_pegin?: boolean;
|
is_pegin?: boolean;
|
||||||
issuance?: Issuance;
|
issuance?: Issuance;
|
||||||
|
// Custom
|
||||||
|
lazy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Issuance {
|
interface Issuance {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user