diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 1f2e3f531..f385b7c20 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -31,7 +31,7 @@ let routes: Routes = [ data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -112,7 +112,7 @@ let routes: Routes = [ data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -153,7 +153,7 @@ let routes: Routes = [ data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -234,7 +234,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { @@ -269,7 +269,7 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { data: { preload: true }, }, { - path: 'wallet', + path: 'widget/wallet', children: [], component: AddressGroupComponent, data: { diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index b893d7e22..41d8c151f 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -117,7 +117,7 @@ - +
diff --git a/frontend/src/app/components/addresses-treemap/addresses-treemap.component.html b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.html new file mode 100644 index 000000000..1c44f9aa3 --- /dev/null +++ b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.html @@ -0,0 +1,10 @@ +
+
+
+
+
+ +
+
+
+
diff --git a/frontend/src/app/components/addresses-treemap/addresses-treemap.component.scss b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.scss new file mode 100644 index 000000000..78510203f --- /dev/null +++ b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.scss @@ -0,0 +1,17 @@ +.node-channels-container { + position: relative; +} + +.loading-spinner { + position: absolute; + top: 0; + left: 0; + right: 0; + width: 100%; + z-index: 100; +} + +.spinner-border { + position: relative; + top: 225px; +} \ No newline at end of file diff --git a/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts new file mode 100644 index 000000000..705941caf --- /dev/null +++ b/frontend/src/app/components/addresses-treemap/addresses-treemap.component.ts @@ -0,0 +1,145 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, NgZone, OnChanges } from '@angular/core'; +import { Router } from '@angular/router'; +import { EChartsOption, TreemapSeriesOption } from '../../graphs/echarts'; +import { lerpColor } from '../../shared/graphs.utils'; +import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe'; +import { LightningApiService } from '../../lightning/lightning-api.service'; +import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; +import { StateService } from '../../services/state.service'; +import { Address } from '../../interfaces/electrs.interface'; +import { formatNumber } from '@angular/common'; + +@Component({ + selector: 'app-addresses-treemap', + templateUrl: './addresses-treemap.component.html', + styleUrls: ['./addresses-treemap.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AddressesTreemap implements OnChanges { + @Input() addresses: Address[]; + @Input() isLoading: boolean = false; + + chartInstance: any; + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private lightningApiService: LightningApiService, + private amountShortenerPipe: AmountShortenerPipe, + private zone: NgZone, + private router: Router, + public stateService: StateService, + ) {} + + ngOnChanges(): void { + this.prepareChartOptions(); + } + + prepareChartOptions(): void { + const maxTxs = this.addresses.reduce((max, address) => Math.max(max, address.chain_stats.tx_count), 0); + const data = this.addresses.map(address => ({ + address: address.address, + value: address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum, + stats: address.chain_stats, + itemStyle: { + color: lerpColor('#1E88E5', '#D81B60', address.chain_stats.tx_count / maxTxs), + } + })); + this.chartOptions = { + tooltip: { + trigger: 'item', + textStyle: { + align: 'left', + } + }, + series: [ + { + height: 300, + left: 0, + right: 0, + bottom: 0, + top: 0, + roam: false, + type: 'treemap', + data: data, + nodeClick: 'link', + progressive: 100, + tooltip: { + show: true, + backgroundColor: 'rgba(17, 19, 31, 1)', + borderRadius: 4, + shadowColor: 'rgba(0, 0, 0, 0.5)', + textStyle: { + color: '#b1b1b1', + }, + borderColor: '#000', + formatter: (value): string => { + if (!value.data.address) { + return ''; + } + return ` + + + + + + + + + + + + + + + + + + + + + + +
${value.data.address}
Recieved${this.formatValue(value.data.stats.funded_txo_sum)}
Sent${this.formatValue(value.data.stats.spent_txo_sum)}
Balance${this.formatValue(value.data.stats.funded_txo_sum - value.data.stats.spent_txo_sum)}
Transaction count${value.data.stats.tx_count}
+ `; + } + }, + itemStyle: { + borderColor: 'black', + borderWidth: 1, + }, + breadcrumb: { + show: false, + } + } + ] + }; + } + + formatValue(sats: number): string { + if (sats > 100000000) { + return formatNumber(sats / 100000000, this.locale, '1.2-2') + ' BTC'; + } else { + return this.amountShortenerPipe.transform(sats, 2) + ' sats'; + } + } + + onChartInit(ec: any): void { + this.chartInstance = ec; + + this.chartInstance.on('click', (e) => { + //@ts-ignore + if (!e.data.address) { + return; + } + this.zone.run(() => { + //@ts-ignore + const url = new RelativeUrlPipe(this.stateService).transform(`/address/${e.data.address}`); + this.router.navigate([url]); + }); + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.html b/frontend/src/app/components/transactions-list/transactions-list.component.html index 217eab7d7..bc38e0dd3 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.html +++ b/frontend/src/app/components/transactions-list/transactions-list.component.html @@ -23,7 +23,7 @@ @@ -214,7 +214,7 @@ @@ -353,7 +353,7 @@ - + +
+ + + + + +
+
+
+ + + + + + + + + + + + +
+
+
+
+ +
+
+
+ +
+ + +
+ +
+ Error loading wallet data. +
+ + There many transactions in this wallet, more than your backend can handle. See more on
setting up a stronger backend. +

+ Consider viewing this wallet on the official Mempool website instead: + +
+ https://mempool.space/wallet?addresses={{ addressStrings.join(',') }} +
+ http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/wallet?addresses={{ addressStrings.join(',') }} +

+ ({{ error | httpErrorMsg }}) +
+ + + + Error loading wallet data. + + + + + + +
+ + + Confidential + + + +
+ +
+
diff --git a/frontend/src/app/components/wallet/wallet.component.scss b/frontend/src/app/components/wallet/wallet.component.scss new file mode 100644 index 000000000..6723cffbc --- /dev/null +++ b/frontend/src/app/components/wallet/wallet.component.scss @@ -0,0 +1,117 @@ +.qr-wrapper { + background-color: #FFF; + padding: 10px; + padding-bottom: 5px; + display: inline-block; +} + +.treemap-col { + width: 45%; + height: 300px; +} + +.fiat { + display: block; + @media (min-width: 992px){ + display: inline-block; + margin-left: 10px; + } +} + +.table { + tr td { + &:last-child { + text-align: right; + @media (min-width: 576px) { + text-align: left; + } + } + } +} + +.address-list { + width: 100%; + max-width: 200px; +} + +h1 { + margin: 0px; + padding: 0px; + margin-right: 10px; + font-size: 1.9rem; + @media (min-width: 576px) { + font-size: 2rem; + float: left; + } + @media (min-width: 768px) { + font-size: 2.5rem; + } +} + +.title-address { + align-items: baseline; +} + +.address-link { + line-height: 56px; + margin-left: 0px; + top: -2px; + position: relative; + @media (min-width: 768px) { + line-height: 69px; + } +} + +.row{ + flex-direction: column; + @media (min-width: 576px) { + flex-direction: row; + } +} + +@media (max-width: 767.98px) { + .mobile-bottomcol { + margin-top: 15px; + } + .details-table td:first-child { + white-space: pre-wrap; + } +} + +.tx-link { + display: block; + height: 100%; + top: 9px; + position: relative; + @media (min-width: 576px) { + top: 11px; + } + @media (min-width: 768px) { + max-width: calc(100% - 180px); + top: 17px; + } +} + +.title-tx { + h2 { + line-height: 1; + margin-bottom: 10px; + } +} + +.liquid-address { + .address-table { + table-layout: fixed; + + tr td:first-child { + width: 170px; + } + tr td:last-child { + width: 80%; + } + } + + .qrcode-col { + flex-grow: 0.5; + } +} diff --git a/frontend/src/app/components/wallet/wallet.component.ts b/frontend/src/app/components/wallet/wallet.component.ts new file mode 100644 index 000000000..e91def889 --- /dev/null +++ b/frontend/src/app/components/wallet/wallet.component.ts @@ -0,0 +1,360 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ElectrsApiService } from '../../services/electrs-api.service'; +import { switchMap, filter, catchError, map, tap, share } from 'rxjs/operators'; +import { Address, Transaction } from '../../interfaces/electrs.interface'; +import { WebsocketService } from '../../services/websocket.service'; +import { StateService } from '../../services/state.service'; +import { AudioService } from '../../services/audio.service'; +import { ApiService } from '../../services/api.service'; +import { of, merge, Subscription, Observable, combineLatest, forkJoin } from 'rxjs'; +import { SeoService } from '../../services/seo.service'; +import { seoDescriptionNetwork } from '../../shared/common.utils'; + +@Component({ + selector: 'app-wallet', + templateUrl: './wallet.component.html', + styleUrls: ['./wallet.component.scss'] +}) +export class WalletComponent implements OnInit, OnDestroy { + network = ''; + + addresses: Address[]; + addressStrings: string[]; + isLoadingAddress = true; + transactions: Transaction[]; + isLoadingTransactions = true; + retryLoadMore = false; + error: any; + mainSubscription: Subscription; + wsSubscription: Subscription; + addressLoadingStatus$: Observable; + + collapseAddresses: boolean = true; + + fullyLoaded = false; + txCount = 0; + received = 0; + sent = 0; + chainBalance = 0; + + private tempTransactions: Transaction[]; + private timeTxIndexes: number[]; + private lastTransactionTxId: string; + + constructor( + private route: ActivatedRoute, + private electrsApiService: ElectrsApiService, + private websocketService: WebsocketService, + private stateService: StateService, + private audioService: AudioService, + private apiService: ApiService, + private seoService: SeoService, + ) { } + + ngOnInit(): void { + this.stateService.networkChanged$.subscribe((network) => this.network = network); + this.websocketService.want(['blocks']); + + const addresses$ = this.route.queryParamMap.pipe( + map((queryParams) => (queryParams.get('addresses') as string)?.split(',').map(this.normalizeAddress)), + tap(addresses => { + this.addressStrings = addresses; + this.error = undefined; + this.isLoadingAddress = true; + this.fullyLoaded = false; + this.addresses = []; + this.isLoadingTransactions = true; + this.transactions = null; + document.body.scrollTo(0, 0); + const titleLabel = addresses[0] + (addresses.length > 1 ? ` +${addresses.length - 1} addresses` : ''); + this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${titleLabel}}:INTERPOLATION:`); + this.seoService.setDescription($localize`:@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${this.stateService.network==='liquid'||this.stateService.network==='liquidtestnet'?'Liquid':'Bitcoin'}${seoDescriptionNetwork(this.stateService.network)} address ${titleLabel}:INTERPOLATION:.`); + }), + share() + ); + + this.addressLoadingStatus$ = addresses$ + .pipe( + switchMap(() => this.stateService.loadingIndicators$), + map((indicators) => indicators['address-' + this.addressStrings.join(',')] !== undefined ? indicators['address-' + this.addressStrings.join(',')] : 0) + ); + + this.mainSubscription = combineLatest([ + addresses$, + merge( + of(true), + this.stateService.connectionState$.pipe(filter((state) => state === 2 && this.transactions && this.transactions.length > 0)), + ), + ]).pipe( + switchMap(([addresses]) => { + return forkJoin( + addresses.map((address) => + address.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/) + ? this.electrsApiService.getPubKeyAddress$(address) + : this.electrsApiService.getAddress$(address) + ) + ); + }), + tap((addresses: Address[]) => { + this.addresses = addresses; + this.updateChainStats(); + this.isLoadingAddress = false; + this.isLoadingTransactions = true; + this.websocketService.startTrackAddresses(addresses.map(address => address.address)); + }), + switchMap((addresses) => { + return addresses[0].is_pubkey + ? this.electrsApiService.getScriptHashesTransactions$(addresses.map(address => (address.address.length === 66 ? '21' : '41') + address.address + 'ac')) + : this.electrsApiService.getAddressesTransactions$(addresses.map(address => address.address)); + }), + switchMap((transactions) => { + this.tempTransactions = transactions; + if (transactions.length) { + this.lastTransactionTxId = transactions[transactions.length - 1].txid; + } + + const fetchTxs: string[] = []; + this.timeTxIndexes = []; + transactions.forEach((tx, index) => { + if (!tx.status.confirmed) { + fetchTxs.push(tx.txid); + this.timeTxIndexes.push(index); + } + }); + if (!fetchTxs.length) { + return of([]); + } + return this.apiService.getTransactionTimes$(fetchTxs).pipe( + catchError((err) => { + this.isLoadingAddress = false; + this.isLoadingTransactions = false; + this.error = err; + this.seoService.logSoft404(); + console.log(err); + return of([]); + }) + ); + }) + ) + .subscribe((times: number[] | null) => { + if (!times) { + return; + } + times.forEach((time, index) => { + this.tempTransactions[this.timeTxIndexes[index]].firstSeen = time; + }); + this.tempTransactions.sort((a, b) => { + if (b.status.confirmed) { + if (b.status.block_height === a.status.block_height) { + return b.status.block_time - a.status.block_time; + } + return b.status.block_height - a.status.block_height; + } + return b.firstSeen - a.firstSeen; + }); + + this.transactions = this.tempTransactions; + this.isLoadingTransactions = false; + }, + (error) => { + console.log(error); + this.error = error; + this.seoService.logSoft404(); + this.isLoadingAddress = false; + }); + + this.wsSubscription = this.stateService.multiAddressTransactions$.subscribe(update => { + for (const address of Object.keys(update)) { + for (const transaction of update[address].mempool) { + this.addTransaction(transaction); + } + for (const transaction of update[address].confirmed) { + const tx = this.transactions.find((t) => t.txid === transaction.txid); + if (tx) { + this.removeTransaction(tx); + tx.status = transaction.status; + this.transactions = this.transactions.slice(); + this.audioService.playSound('magic'); + } else { + if (this.addTransaction(transaction, false)) { + this.audioService.playSound('magic'); + } + } + } + for (const transaction of update[address].removed) { + this.removeTransaction(transaction); + } + } + }); + } + + addTransaction(transaction: Transaction, playSound: boolean = true): boolean { + if (this.transactions.some((t) => t.txid === transaction.txid)) { + return false; + } + + this.transactions.unshift(transaction); + this.transactions = this.transactions.slice(); + this.txCount++; + + if (playSound) { + if (transaction.vout.some((vout) => this.addressStrings.includes(vout?.scriptpubkey_address))) { + this.audioService.playSound('cha-ching'); + } else { + this.audioService.playSound('chime'); + } + } + + for (const address of this.addresses) { + let match = false; + transaction.vin.forEach((vin) => { + if (vin?.prevout?.scriptpubkey_address === address.address) { + match = true; + this.sent += vin.prevout.value; + if (transaction.status?.confirmed) { + address.chain_stats.funded_txo_count++; + address.chain_stats.funded_txo_sum += vin.prevout.value; + } else { + address.mempool_stats.funded_txo_count++; + address.mempool_stats.funded_txo_sum += vin.prevout.value; + } + } + }); + transaction.vout.forEach((vout) => { + match = true; + if (vout?.scriptpubkey_address === address.address) { + this.received += vout.value; + } + if (transaction.status?.confirmed) { + address.chain_stats.spent_txo_count++; + address.chain_stats.spent_txo_sum += vout.value; + } else { + address.mempool_stats.spent_txo_count++; + address.mempool_stats.spent_txo_sum += vout.value; + } + }); + if (match) { + if (transaction.status?.confirmed) { + address.chain_stats.tx_count++; + } else { + address.mempool_stats.tx_count++; + } + } + } + + return true; + } + + removeTransaction(transaction: Transaction): boolean { + const index = this.transactions.findIndex(((tx) => tx.txid === transaction.txid)); + if (index === -1) { + return false; + } + + this.transactions.splice(index, 1); + this.transactions = this.transactions.slice(); + this.txCount--; + + for (const address of this.addresses) { + let match = false; + transaction.vin.forEach((vin) => { + if (vin?.prevout?.scriptpubkey_address === address.address) { + match = true; + this.sent -= vin.prevout.value; + if (transaction.status?.confirmed) { + address.chain_stats.funded_txo_count--; + address.chain_stats.funded_txo_sum -= vin.prevout.value; + } else { + address.mempool_stats.funded_txo_count--; + address.mempool_stats.funded_txo_sum -= vin.prevout.value; + } + } + }); + transaction.vout.forEach((vout) => { + match = true; + if (vout?.scriptpubkey_address === address.address) { + this.received -= vout.value; + } + if (transaction.status?.confirmed) { + address.chain_stats.spent_txo_count--; + address.chain_stats.spent_txo_sum -= vout.value; + } else { + address.mempool_stats.spent_txo_count--; + address.mempool_stats.spent_txo_sum -= vout.value; + } + }); + if (match) { + if (transaction.status?.confirmed) { + address.chain_stats.tx_count--; + } else { + address.mempool_stats.tx_count--; + } + } + } + + return true; + } + + loadMore(): void { + if (this.isLoadingTransactions || this.fullyLoaded) { + return; + } + this.isLoadingTransactions = true; + this.retryLoadMore = false; + + (this.addresses[0].is_pubkey + ? this.electrsApiService.getScriptHashesTransactions$(this.addresses.map(address => (address.address.length === 66 ? '21' : '41') + address.address + 'ac'), this.lastTransactionTxId) + : this.electrsApiService.getAddressesTransactions$(this.addresses.map(address => address.address), this.lastTransactionTxId) + ).pipe( + catchError((error) => { + this.isLoadingTransactions = false; + this.retryLoadMore = true; + // In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page. + if (error.status === 422) { + window.location.reload(); + } + return of([]); + }) + ).subscribe((transactions: Transaction[]) => { + if (transactions && transactions.length) { + this.lastTransactionTxId = transactions[transactions.length - 1].txid; + this.transactions = this.transactions.concat(transactions); + } else { + this.fullyLoaded = true; + } + this.isLoadingTransactions = false; + }); + } + + normalizeAddress(address: string): string { + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) { + return address.toLowerCase(); + } else { + return address; + } + } + + updateChainStats(): void { + let received = 0; + let sent = 0; + let txCount = 0; + let chainBalance = 0; + for (const address of this.addresses) { + received += address.chain_stats.funded_txo_sum + address.mempool_stats.funded_txo_sum; + sent += address.chain_stats.spent_txo_sum + address.mempool_stats.spent_txo_sum; + txCount += address.chain_stats.tx_count + address.mempool_stats.tx_count; + chainBalance += (address.chain_stats.funded_txo_sum - address.chain_stats.spent_txo_sum); + } + this.received = received; + this.sent = sent; + this.txCount = txCount; + this.chainBalance = chainBalance; + } + + ngOnDestroy(): void { + this.mainSubscription.unsubscribe(); + this.websocketService.stopTrackingAddresses(); + this.wsSubscription.unsubscribe(); + } +} diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts index ee51069c5..10f0b4d6c 100644 --- a/frontend/src/app/graphs/graphs.module.ts +++ b/frontend/src/app/graphs/graphs.module.ts @@ -35,9 +35,11 @@ import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-ch import { HashrateChartPoolsComponent } from '../components/hashrates-chart-pools/hashrate-chart-pools.component'; import { BlockHealthGraphComponent } from '../components/block-health-graph/block-health-graph.component'; import { AddressComponent } from '../components/address/address.component'; +import { WalletComponent } from '../components/wallet/wallet.component'; import { AddressGraphComponent } from '../components/address-graph/address-graph.component'; import { UtxoGraphComponent } from '../components/utxo-graph/utxo-graph.component'; import { ActiveAccelerationBox } from '../components/acceleration/active-acceleration-box/active-acceleration-box.component'; +import { AddressesTreemap } from '../components/addresses-treemap/addresses-treemap.component'; import { CommonModule } from '@angular/common'; @NgModule({ @@ -46,6 +48,7 @@ import { CommonModule } from '@angular/common'; CustomDashboardComponent, MempoolBlockComponent, AddressComponent, + WalletComponent, MiningDashboardComponent, AcceleratorDashboardComponent, @@ -79,6 +82,7 @@ import { CommonModule } from '@angular/common'; AddressGraphComponent, UtxoGraphComponent, ActiveAccelerationBox, + AddressesTreemap, ], imports: [ CommonModule, diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts index 40bf64144..5e7707a89 100644 --- a/frontend/src/app/graphs/graphs.routing.module.ts +++ b/frontend/src/app/graphs/graphs.routing.module.ts @@ -22,6 +22,7 @@ import { CustomDashboardComponent } from '../components/custom-dashboard/custom- import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component'; import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component'; import { AddressComponent } from '../components/address/address.component'; +import { WalletComponent } from '../components/wallet/wallet.component'; const browserWindow = window || {}; // @ts-ignore @@ -88,6 +89,15 @@ const routes: Routes = [ networkSpecific: true, } }, + { + path: 'wallet', + children: [], + component: WalletComponent, + data: { + ogImage: true, + networkSpecific: true, + } + }, { path: 'graphs', data: { networks: ['bitcoin', 'liquid'] }, diff --git a/frontend/src/app/services/electrs-api.service.ts b/frontend/src/app/services/electrs-api.service.ts index f1468f8aa..2ae9138b3 100644 --- a/frontend/src/app/services/electrs-api.service.ts +++ b/frontend/src/app/services/electrs-api.service.ts @@ -142,6 +142,14 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs', { params }); } + getAddressesTransactions$(addresses: string[], txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs?addresses=${addresses.join(',')}`, { params }); + } + getAddressSummary$(address: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { @@ -150,6 +158,14 @@ export class ElectrsApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs/summary', { params }); } + getAddressesSummary$(addresses: string[], txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/addresses/txs/summary?addresses=${addresses.join(',')}`, { params }); + } + getScriptHashTransactions$(script: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { @@ -160,6 +176,16 @@ export class ElectrsApiService { ); } + getScriptHashesTransactions$(scripts: string[], txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe( + switchMap(scriptHashes => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs?scripthashes=${scriptHashes.join(',')}`, { params })), + ); + } + getScriptHashSummary$(script: string, txid?: string): Observable { let params = new HttpParams(); if (txid) { @@ -180,6 +206,16 @@ export class ElectrsApiService { ); } + getScriptHashesSummary$(scripts: string[], txid?: string): Observable { + let params = new HttpParams(); + if (txid) { + params = params.append('after_txid', txid); + } + return from(Promise.all(scripts.map(script => calcScriptHash$(script)))).pipe( + switchMap(scriptHashes => this.httpClient.get(this.apiBaseUrl + this.apiBasePath + `/api/scripthashes/txs/summary?scripthashes=${scriptHashes.join(',')}`, { params })), + ); + } + getAsset$(assetId: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId); }