Merge pull request #4027 from mempool/mononaut/p2pk
Support P2PK address types
This commit is contained in:
commit
5e91af168b
@ -14,6 +14,8 @@ export interface AbstractBitcoinApi {
|
|||||||
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||||
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$getAddressPrefix(prefix: string): string[];
|
$getAddressPrefix(prefix: string): string[];
|
||||||
|
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash>;
|
||||||
|
$getScriptHashTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
$sendRawTransaction(rawTransaction: string): Promise<string>;
|
||||||
$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[]>;
|
||||||
|
@ -108,6 +108,14 @@ class BitcoinApi implements AbstractBitcoinApi {
|
|||||||
throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
|
throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
|
||||||
|
throw new Error('Method getScriptHash not supported by the Bitcoin RPC API.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$getScriptHashTransactions(scripthash: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
|
throw new Error('Method getScriptHashTransactions not supported by the Bitcoin RPC API.');
|
||||||
|
}
|
||||||
|
|
||||||
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
|
||||||
return this.bitcoindClient.getRawMemPool();
|
return this.bitcoindClient.getRawMemPool();
|
||||||
}
|
}
|
||||||
|
@ -121,6 +121,8 @@ class BitcoinRoutes {
|
|||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash', this.getScriptHash)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'scripthash/:scripthash/txs', this.getScriptHashTransactions)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
|
.get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', this.getAddressPrefix)
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
@ -567,6 +569,45 @@ class BitcoinRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getScriptHash(req: Request, res: Response) {
|
||||||
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
|
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const addressData = await bitcoinApi.$getScriptHash(req.params.address);
|
||||||
|
res.json(addressData);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
|
return res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getScriptHashTransactions(req: Request, res: Response): Promise<void> {
|
||||||
|
if (config.MEMPOOL.BACKEND === 'none') {
|
||||||
|
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let lastTxId: string = '';
|
||||||
|
if (req.query.after_txid && typeof req.query.after_txid === 'string') {
|
||||||
|
lastTxId = req.query.after_txid;
|
||||||
|
}
|
||||||
|
const transactions = await bitcoinApi.$getScriptHashTransactions(req.params.address, lastTxId);
|
||||||
|
res.json(transactions);
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.message && (e.message.indexOf('too long') > 0 || e.message.indexOf('confirmed status') > 0)) {
|
||||||
|
res.status(413).send(e instanceof Error ? e.message : e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(500).send(e instanceof Error ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async getAddressPrefix(req: Request, res: Response) {
|
private async getAddressPrefix(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
const blockHash = await bitcoinApi.$getAddressPrefix(req.params.prefix);
|
||||||
|
@ -126,6 +126,77 @@ class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async $getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
|
||||||
|
try {
|
||||||
|
const balance = await this.electrumClient.blockchainScripthash_getBalance(scripthash);
|
||||||
|
let history = memoryCache.get<IElectrumApi.ScriptHashHistory[]>('Scripthash_getHistory', scripthash);
|
||||||
|
if (!history) {
|
||||||
|
history = await this.electrumClient.blockchainScripthash_getHistory(scripthash);
|
||||||
|
memoryCache.set('Scripthash_getHistory', scripthash, history, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unconfirmed = history ? history.filter((h) => h.fee).length : 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
'scripthash': scripthash,
|
||||||
|
'chain_stats': {
|
||||||
|
'funded_txo_count': 0,
|
||||||
|
'funded_txo_sum': balance.confirmed ? balance.confirmed : 0,
|
||||||
|
'spent_txo_count': 0,
|
||||||
|
'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0,
|
||||||
|
'tx_count': (history?.length || 0) - unconfirmed,
|
||||||
|
},
|
||||||
|
'mempool_stats': {
|
||||||
|
'funded_txo_count': 0,
|
||||||
|
'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0,
|
||||||
|
'spent_txo_count': 0,
|
||||||
|
'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0,
|
||||||
|
'tx_count': unconfirmed,
|
||||||
|
},
|
||||||
|
'electrum': true,
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(typeof e === 'string' ? e : e && e.message || e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async $getScriptHashTransactions(scripthash: string, lastSeenTxId?: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
|
try {
|
||||||
|
loadingIndicators.setProgress('address-' + scripthash, 0);
|
||||||
|
|
||||||
|
const transactions: IEsploraApi.Transaction[] = [];
|
||||||
|
let history = memoryCache.get<IElectrumApi.ScriptHashHistory[]>('Scripthash_getHistory', scripthash);
|
||||||
|
if (!history) {
|
||||||
|
history = await this.electrumClient.blockchainScripthash_getHistory(scripthash);
|
||||||
|
memoryCache.set('Scripthash_getHistory', scripthash, history, 2);
|
||||||
|
}
|
||||||
|
if (!history) {
|
||||||
|
throw new Error('failed to get scripthash history');
|
||||||
|
}
|
||||||
|
history.sort((a, b) => (b.height || 9999999) - (a.height || 9999999));
|
||||||
|
|
||||||
|
let startingIndex = 0;
|
||||||
|
if (lastSeenTxId) {
|
||||||
|
const pos = history.findIndex((historicalTx) => historicalTx.tx_hash === lastSeenTxId);
|
||||||
|
if (pos) {
|
||||||
|
startingIndex = pos + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const endIndex = Math.min(startingIndex + 10, history.length);
|
||||||
|
|
||||||
|
for (let i = startingIndex; i < endIndex; i++) {
|
||||||
|
const tx = await this.$getRawTransaction(history[i].tx_hash, false, true);
|
||||||
|
transactions.push(tx);
|
||||||
|
loadingIndicators.setProgress('address-' + scripthash, (i + 1) / endIndex * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transactions;
|
||||||
|
} catch (e: any) {
|
||||||
|
loadingIndicators.setProgress('address-' + scripthash, 100);
|
||||||
|
throw new Error(typeof e === 'string' ? e : e && e.message || e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
|
private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
|
||||||
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
|
return this.electrumClient.blockchainScripthash_getBalance(this.encodeScriptHash(scriptHash));
|
||||||
}
|
}
|
||||||
|
@ -99,6 +99,13 @@ export namespace IEsploraApi {
|
|||||||
electrum?: boolean;
|
electrum?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScriptHash {
|
||||||
|
scripthash: string;
|
||||||
|
chain_stats: ChainStats;
|
||||||
|
mempool_stats: MempoolStats;
|
||||||
|
electrum?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChainStats {
|
export interface ChainStats {
|
||||||
funded_txo_count: number;
|
funded_txo_count: number;
|
||||||
funded_txo_sum: number;
|
funded_txo_sum: number;
|
||||||
|
@ -110,6 +110,14 @@ class ElectrsApi implements AbstractBitcoinApi {
|
|||||||
throw new Error('Method getAddressTransactions not implemented.');
|
throw new Error('Method getAddressTransactions not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$getScriptHash(scripthash: string): Promise<IEsploraApi.ScriptHash> {
|
||||||
|
throw new Error('Method getScriptHash not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$getScriptHashTransactions(scripthash: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
|
||||||
|
throw new Error('Method getScriptHashTransactions not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
$getAddressPrefix(prefix: string): string[] {
|
$getAddressPrefix(prefix: string): string[] {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
@ -281,3 +281,15 @@ export function isFeatureActive(network: string, height: number, feature: 'rbf'
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function calcScriptHash$(script: string): Promise<string> {
|
||||||
|
if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
|
||||||
|
throw new Error('script is not a valid hex string');
|
||||||
|
}
|
||||||
|
const buf = Uint8Array.from(script.match(/.{2}/g).map((byte) => parseInt(byte, 16)));
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', buf);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
return hashArray
|
||||||
|
.map((bytes) => bytes.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
@ -64,13 +64,15 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
|
|||||||
this.address = null;
|
this.address = null;
|
||||||
this.addressInfo = null;
|
this.addressInfo = null;
|
||||||
this.addressString = params.get('id') || '';
|
this.addressString = params.get('id') || '';
|
||||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
|
||||||
this.addressString = this.addressString.toLowerCase();
|
this.addressString = this.addressString.toLowerCase();
|
||||||
}
|
}
|
||||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||||
|
|
||||||
return this.electrsApiService.getAddress$(this.addressString)
|
return (this.addressString.match(/[a-f0-9]{130}/)
|
||||||
.pipe(
|
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||||
|
: this.electrsApiService.getAddress$(this.addressString)
|
||||||
|
).pipe(
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.error = err;
|
this.error = err;
|
||||||
|
@ -81,6 +81,7 @@ h1 {
|
|||||||
top: 11px;
|
top: 11px;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
max-width: calc(100% - 180px);
|
||||||
top: 17px;
|
top: 17px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
|
|||||||
import { ActivatedRoute, ParamMap } from '@angular/router';
|
import { ActivatedRoute, ParamMap } from '@angular/router';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
|
||||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
|
import { Address, ScriptHash, Transaction } from '../../interfaces/electrs.interface';
|
||||||
import { WebsocketService } from '../../services/websocket.service';
|
import { WebsocketService } from '../../services/websocket.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { AudioService } from '../../services/audio.service';
|
import { AudioService } from '../../services/audio.service';
|
||||||
@ -72,7 +72,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.addressInfo = null;
|
this.addressInfo = null;
|
||||||
document.body.scrollTo(0, 0);
|
document.body.scrollTo(0, 0);
|
||||||
this.addressString = params.get('id') || '';
|
this.addressString = params.get('id') || '';
|
||||||
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
|
if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|[A-F0-9]{130}$/.test(this.addressString)) {
|
||||||
this.addressString = this.addressString.toLowerCase();
|
this.addressString = this.addressString.toLowerCase();
|
||||||
}
|
}
|
||||||
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
|
||||||
@ -83,8 +83,11 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0))
|
.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0))
|
||||||
)
|
)
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => this.electrsApiService.getAddress$(this.addressString)
|
switchMap(() => (
|
||||||
.pipe(
|
this.addressString.match(/[a-f0-9]{130}/)
|
||||||
|
? this.electrsApiService.getPubKeyAddress$(this.addressString)
|
||||||
|
: this.electrsApiService.getAddress$(this.addressString)
|
||||||
|
).pipe(
|
||||||
catchError((err) => {
|
catchError((err) => {
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.error = err;
|
this.error = err;
|
||||||
@ -114,7 +117,9 @@ export class AddressComponent implements OnInit, OnDestroy {
|
|||||||
this.updateChainStats();
|
this.updateChainStats();
|
||||||
this.isLoadingAddress = false;
|
this.isLoadingAddress = false;
|
||||||
this.isLoadingTransactions = true;
|
this.isLoadingTransactions = true;
|
||||||
return this.electrsApiService.getAddressTransactions$(address.address);
|
return address.is_pubkey
|
||||||
|
? this.electrsApiService.getScriptHashTransactions$('41' + address.address + 'ac')
|
||||||
|
: this.electrsApiService.getAddressTransactions$(address.address);
|
||||||
}),
|
}),
|
||||||
switchMap((transactions) => {
|
switchMap((transactions) => {
|
||||||
this.tempTransactions = transactions;
|
this.tempTransactions = transactions;
|
||||||
|
@ -34,7 +34,7 @@ export class SearchFormComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59})$/;
|
regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[A-z]{2,5}1[a-zA-HJ-NP-Z0-9]{39,59}|[0-9a-fA-F]{130})$/;
|
||||||
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
regexBlockhash = /^[0]{8}[a-fA-F0-9]{56}$/;
|
||||||
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
regexTransaction = /^([a-fA-F0-9]{64})(:\d+)?$/;
|
||||||
regexBlockheight = /^[0-9]{1,9}$/;
|
regexBlockheight = /^[0-9]{1,9}$/;
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
<ng-template ngFor let-vin let-vindex="index" [ngForOf]="tx.vin.slice(0, getVinLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||||
<tr [ngClass]="{
|
<tr [ngClass]="{
|
||||||
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
'assetBox': (assetsMinimal && vin.prevout && assetsMinimal[vin.prevout.asset] && !vin.is_coinbase && vin.prevout.scriptpubkey_address && tx._unblinded) || inputIndex === vindex,
|
||||||
'highlight': vin.prevout?.scriptpubkey_address === this.address && this.address !== ''
|
'highlight': this.address !== '' && (vin.prevout?.scriptpubkey_address === this.address || (vin.prevout?.scriptpubkey_type === 'p2pk' && vin.prevout?.scriptpubkey.slice(2, 132) === this.address))
|
||||||
}">
|
}">
|
||||||
<td class="arrow-td">
|
<td class="arrow-td">
|
||||||
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
|
||||||
@ -56,7 +56,9 @@
|
|||||||
<span i18n="transactions-list.peg-in">Peg-in</span>
|
<span i18n="transactions-list.peg-in">Peg-in</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
|
<ng-container *ngSwitchCase="vin.prevout && vin.prevout.scriptpubkey_type === 'p2pk'">
|
||||||
<span>P2PK</span>
|
<span>P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vin.prevout.scriptpubkey.slice(2, 132)]" title="{{ vin.prevout.scriptpubkey.slice(2, 132) }}">
|
||||||
|
<app-truncate [text]="vin.prevout.scriptpubkey.slice(2, 132)" [lastChars]="8"></app-truncate>
|
||||||
|
</a></span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngSwitchDefault>
|
<ng-container *ngSwitchDefault>
|
||||||
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
<ng-template [ngIf]="!vin.prevout" [ngIfElse]="defaultAddress">
|
||||||
@ -182,12 +184,19 @@
|
|||||||
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx.vout.slice(0, getVoutLimit(tx))" [ngForTrackBy]="trackByIndexFn">
|
||||||
<tr [ngClass]="{
|
<tr [ngClass]="{
|
||||||
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
'assetBox': assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address && tx.vin && !tx.vin[0].is_coinbase && tx._unblinded || outputIndex === vindex,
|
||||||
'highlight': vout.scriptpubkey_address === this.address && this.address !== ''
|
'highlight': this.address !== '' && (vout.scriptpubkey_address === this.address || (vout.scriptpubkey_type === 'p2pk' && vout.scriptpubkey.slice(2, 132) === this.address))
|
||||||
}">
|
}">
|
||||||
<td class="address-cell">
|
<td class="address-cell">
|
||||||
<a class="address" *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
<a class="address" *ngIf="vout.scriptpubkey_address; else pubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
|
||||||
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
<app-truncate [text]="vout.scriptpubkey_address" [lastChars]="8"></app-truncate>
|
||||||
</a>
|
</a>
|
||||||
|
<ng-template #pubkey_type>
|
||||||
|
<ng-container *ngIf="vout.scriptpubkey_type === 'p2pk'; else scriptpubkey_type">
|
||||||
|
P2PK <a class="address p2pk-address" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey.slice(2, 132)]" title="{{ vout.scriptpubkey.slice(2, 132) }}">
|
||||||
|
<app-truncate [text]="vout.scriptpubkey.slice(2, 132)" [lastChars]="8"></app-truncate>
|
||||||
|
</a>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
||||||
<div>
|
<div>
|
||||||
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
<app-address-labels [vout]="vout" [channel]="tx._channels && tx._channels.outputs[vindex] ? tx._channels.outputs[vindex] : null"></app-address-labels>
|
||||||
</div>
|
</div>
|
||||||
|
@ -140,6 +140,15 @@ h2 {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p2pk-address {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 1em;
|
||||||
|
max-width: 100px;
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
max-width: 200px
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.grey-info-text {
|
.grey-info-text {
|
||||||
color:#6c757d;
|
color:#6c757d;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
@ -129,6 +129,22 @@ export interface Address {
|
|||||||
address: string;
|
address: string;
|
||||||
chain_stats: ChainStats;
|
chain_stats: ChainStats;
|
||||||
mempool_stats: MempoolStats;
|
mempool_stats: MempoolStats;
|
||||||
|
is_pubkey?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptHash {
|
||||||
|
electrum?: boolean;
|
||||||
|
scripthash: string;
|
||||||
|
chain_stats: ChainStats;
|
||||||
|
mempool_stats: MempoolStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddressOrScriptHash {
|
||||||
|
electrum?: boolean;
|
||||||
|
address?: string;
|
||||||
|
scripthash?: string;
|
||||||
|
chain_stats: ChainStats;
|
||||||
|
mempool_stats: MempoolStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChainStats {
|
export interface ChainStats {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, from, of, switchMap } from 'rxjs';
|
||||||
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
|
import { Transaction, Address, Outspend, Recent, Asset, ScriptHash } from '../interfaces/electrs.interface';
|
||||||
import { StateService } from './state.service';
|
import { StateService } from './state.service';
|
||||||
import { BlockExtended } from '../interfaces/node-api.interface';
|
import { BlockExtended } from '../interfaces/node-api.interface';
|
||||||
|
import { calcScriptHash$ } from '../bitcoin.utils';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -65,6 +66,24 @@ export class ElectrsApiService {
|
|||||||
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPubKeyAddress$(pubkey: string): Observable<Address> {
|
||||||
|
return this.getScriptHash$('41' + pubkey + 'ac').pipe(
|
||||||
|
switchMap((scripthash: ScriptHash) => {
|
||||||
|
return of({
|
||||||
|
...scripthash,
|
||||||
|
address: pubkey,
|
||||||
|
is_pubkey: true,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getScriptHash$(script: string): Observable<ScriptHash> {
|
||||||
|
return from(calcScriptHash$(script)).pipe(
|
||||||
|
switchMap(scriptHash => this.httpClient.get<ScriptHash>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getAddressTransactions$(address: string, txid?: string): Observable<Transaction[]> {
|
getAddressTransactions$(address: string, txid?: string): Observable<Transaction[]> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
if (txid) {
|
if (txid) {
|
||||||
@ -73,6 +92,16 @@ export class ElectrsApiService {
|
|||||||
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getScriptHashTransactions$(script: string, txid?: string): Observable<Transaction[]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
if (txid) {
|
||||||
|
params = params.append('after_txid', txid);
|
||||||
|
}
|
||||||
|
return from(calcScriptHash$(script)).pipe(
|
||||||
|
switchMap(scriptHash => this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/scripthash/' + scriptHash + '/txs', { params })),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
getAsset$(assetId: string): Observable<Asset> {
|
getAsset$(assetId: string): Observable<Asset> {
|
||||||
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
|
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user