diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index 28faa9595..000a0f177 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -6,6 +6,7 @@ import { BlockComponent } from './components/block/block.component';
import { BlockAuditComponent } from './components/block-audit/block-audit.component';
import { BlockPreviewComponent } from './components/block/block-preview.component';
import { AddressComponent } from './components/address/address.component';
+import { AddressPreviewComponent } from './components/address/address-preview.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component';
import { AboutComponent } from './components/about/about.component';
@@ -69,7 +70,10 @@ let routes: Routes = [
{
path: 'address/:id',
children: [],
- component: AddressComponent
+ component: AddressComponent,
+ data: {
+ ogImage: true
+ }
},
{
path: 'tx',
@@ -175,7 +179,10 @@ let routes: Routes = [
{
path: 'address/:id',
children: [],
- component: AddressComponent
+ component: AddressComponent,
+ data: {
+ ogImage: true
+ }
},
{
path: 'tx',
@@ -278,7 +285,10 @@ let routes: Routes = [
{
path: 'address/:id',
children: [],
- component: AddressComponent
+ component: AddressComponent,
+ data: {
+ ogImage: true
+ }
},
{
path: 'tx',
@@ -342,6 +352,21 @@ let routes: Routes = [
path: 'signet/block/:id',
component: BlockPreviewComponent
},
+ {
+ path: 'address/:id',
+ children: [],
+ component: AddressPreviewComponent
+ },
+ {
+ path: 'testnet/address/:id',
+ children: [],
+ component: AddressPreviewComponent
+ },
+ {
+ path: 'signet/address/:id',
+ children: [],
+ component: AddressPreviewComponent
+ },
],
},
{
@@ -415,7 +440,10 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: 'address/:id',
children: [],
- component: AddressComponent
+ component: AddressComponent,
+ data: {
+ ogImage: true
+ }
},
{
path: 'tx',
@@ -522,7 +550,10 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
{
path: 'address/:id',
children: [],
- component: AddressComponent
+ component: AddressComponent,
+ data: {
+ ogImage: true
+ }
},
{
path: 'tx',
@@ -595,6 +626,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') {
path: 'testnet/block/:id',
component: BlockPreviewComponent
},
+ {
+ path: 'address/:id',
+ children: [],
+ component: AddressPreviewComponent
+ },
+ {
+ path: 'testnet/address/:id',
+ children: [],
+ component: AddressPreviewComponent
+ },
],
},
{
diff --git a/frontend/src/app/components/address/address-preview.component.html b/frontend/src/app/components/address/address-preview.component.html
new file mode 100644
index 000000000..bc73d064b
--- /dev/null
+++ b/frontend/src/app/components/address/address-preview.component.html
@@ -0,0 +1,55 @@
+
+
+
+ Confidential |
+
diff --git a/frontend/src/app/components/address/address-preview.component.scss b/frontend/src/app/components/address/address-preview.component.scss
new file mode 100644
index 000000000..f286c6ca1
--- /dev/null
+++ b/frontend/src/app/components/address/address-preview.component.scss
@@ -0,0 +1,46 @@
+h1 {
+ font-size: 42px;
+ margin: 0;
+}
+
+.qr-wrapper {
+ background-color: #FFF;
+ padding: 10px;
+ padding-bottom: 5px;
+ display: inline-block;
+}
+
+.qrcode-col {
+ width: 420px;
+ min-width: 420px;
+ flex-grow: 0;
+ flex-shrink: 0;
+ text-align: center;
+}
+
+.table {
+ font-size: 24px;
+
+ ::ng-deep .symbol {
+ font-size: 18px;
+ }
+}
+
+.address-link {
+ font-size: 20px;
+ margin-bottom: 0.5em;
+ display: flex;
+ flex-direction: row;
+ align-items: baseline;
+ .truncated-address {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ max-width: calc(505px - 4em);
+ display: inline-block;
+ white-space: nowrap;
+ }
+ .last-four {
+ display: inline-block;
+ white-space: nowrap;
+ }
+}
diff --git a/frontend/src/app/components/address/address-preview.component.ts b/frontend/src/app/components/address/address-preview.component.ts
new file mode 100644
index 000000000..c661c29db
--- /dev/null
+++ b/frontend/src/app/components/address/address-preview.component.ts
@@ -0,0 +1,116 @@
+import { Component, OnInit, OnDestroy } from '@angular/core';
+import { ActivatedRoute, ParamMap } from '@angular/router';
+import { ElectrsApiService } from '../../services/electrs-api.service';
+import { switchMap, filter, catchError, map, tap } from 'rxjs/operators';
+import { Address, Transaction } from '../../interfaces/electrs.interface';
+import { StateService } from 'src/app/services/state.service';
+import { OpenGraphService } from 'src/app/services/opengraph.service';
+import { AudioService } from 'src/app/services/audio.service';
+import { ApiService } from 'src/app/services/api.service';
+import { of, merge, Subscription, Observable } from 'rxjs';
+import { SeoService } from 'src/app/services/seo.service';
+import { AddressInformation } from 'src/app/interfaces/node-api.interface';
+
+@Component({
+ selector: 'app-address-preview',
+ templateUrl: './address-preview.component.html',
+ styleUrls: ['./address-preview.component.scss']
+})
+export class AddressPreviewComponent implements OnInit, OnDestroy {
+ network = '';
+
+ address: Address;
+ addressString: string;
+ isLoadingAddress = true;
+ error: any;
+ mainSubscription: Subscription;
+ addressLoadingStatus$: Observable;
+ addressInfo: null | AddressInformation = null;
+
+ totalConfirmedTxCount = 0;
+ loadedConfirmedTxCount = 0;
+ txCount = 0;
+ received = 0;
+ sent = 0;
+ totalUnspent = 0;
+
+ constructor(
+ private route: ActivatedRoute,
+ private electrsApiService: ElectrsApiService,
+ private stateService: StateService,
+ private apiService: ApiService,
+ private seoService: SeoService,
+ private openGraphService: OpenGraphService,
+ ) { }
+
+ ngOnInit() {
+ this.openGraphService.setPreviewLoading();
+ this.stateService.networkChanged$.subscribe((network) => this.network = network);
+
+ this.addressLoadingStatus$ = this.route.paramMap
+ .pipe(
+ switchMap(() => this.stateService.loadingIndicators$),
+ map((indicators) => indicators['address-' + this.addressString] !== undefined ? indicators['address-' + this.addressString] : 0)
+ );
+
+ this.mainSubscription = this.route.paramMap
+ .pipe(
+ switchMap((params: ParamMap) => {
+ this.error = undefined;
+ this.isLoadingAddress = true;
+ this.loadedConfirmedTxCount = 0;
+ this.address = null;
+ this.addressInfo = null;
+ this.addressString = params.get('id') || '';
+ if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) {
+ this.addressString = this.addressString.toLowerCase();
+ }
+ this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`);
+
+ return this.electrsApiService.getAddress$(this.addressString)
+ .pipe(
+ catchError((err) => {
+ this.isLoadingAddress = false;
+ this.error = err;
+ console.log(err);
+ return of(null);
+ })
+ );
+ })
+ )
+ .pipe(
+ filter((address) => !!address),
+ tap((address: Address) => {
+ if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) {
+ this.apiService.validateAddress$(address.address)
+ .subscribe((addressInfo) => {
+ this.addressInfo = addressInfo;
+ });
+ }
+ this.address = address;
+ this.updateChainStats();
+ this.isLoadingAddress = false;
+ this.openGraphService.setPreviewReady();
+ })
+ )
+ .subscribe(() => {},
+ (error) => {
+ console.log(error);
+ this.error = error;
+ this.isLoadingAddress = false;
+ }
+ );
+ }
+
+ updateChainStats() {
+ this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum;
+ this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum;
+ this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count;
+ this.totalConfirmedTxCount = this.address.chain_stats.tx_count;
+ this.totalUnspent = this.address.chain_stats.funded_txo_count - this.address.chain_stats.spent_txo_count;
+ }
+
+ ngOnDestroy() {
+ this.mainSubscription.unsubscribe();
+ }
+}
diff --git a/frontend/src/app/components/block/block-preview.component.scss b/frontend/src/app/components/block/block-preview.component.scss
index 6099f5d47..f2049a1d3 100644
--- a/frontend/src/app/components/block/block-preview.component.scss
+++ b/frontend/src/app/components/block/block-preview.component.scss
@@ -1,7 +1,3 @@
-.box {
- padding: 2rem 3rem;
-}
-
.block-title {
margin-bottom: 0.75em;
font-size: 42px;
diff --git a/frontend/src/app/services/opengraph.service.ts b/frontend/src/app/services/opengraph.service.ts
index 48064fdea..ad62a889c 100644
--- a/frontend/src/app/services/opengraph.service.ts
+++ b/frontend/src/app/services/opengraph.service.ts
@@ -58,4 +58,14 @@ export class OpenGraphService {
this.metaService.updateTag({ property: 'og:image:width', content: '1000' });
this.metaService.updateTag({ property: 'og:image:height', content: '500' });
}
+
+ /// signal that the unfurler should wait for a 'ready' signal before taking a screenshot
+ setPreviewLoading() {
+ this.metaService.updateTag({ property: 'og:loading', content: 'loading'});
+ }
+
+ // signal to the unfurler that the page is ready for a screenshot
+ setPreviewReady() {
+ this.metaService.updateTag({ property: 'og:ready', content: 'ready'});
+ }
}
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index cd087a3c4..fc7acec54 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -50,6 +50,7 @@ import { BlockAuditComponent } from '../components/block-audit/block-audit.compo
import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component';
import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component';
import { AddressComponent } from '../components/address/address.component';
+import { AddressPreviewComponent } from '../components/address/address-preview.component';
import { SearchFormComponent } from '../components/search-form/search-form.component';
import { AddressLabelsComponent } from '../components/address-labels/address-labels.component';
import { FooterComponent } from '../components/footer/footer.component';
@@ -124,6 +125,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
BlockOverviewTooltipComponent,
TransactionsListComponent,
AddressComponent,
+ AddressPreviewComponent,
SearchFormComponent,
TimeSpanComponent,
AddressLabelsComponent,
@@ -225,6 +227,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
BlockOverviewTooltipComponent,
TransactionsListComponent,
AddressComponent,
+ AddressPreviewComponent,
SearchFormComponent,
TimeSpanComponent,
AddressLabelsComponent,
diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss
index d5e8dde28..da4bdcffe 100644
--- a/frontend/src/styles.scss
+++ b/frontend/src/styles.scss
@@ -87,6 +87,11 @@ body {
box-shadow: 0.125rem 0.125rem 0.25rem rgba(0,0,0,0.075);
}
+.preview-box {
+ min-height: 512px;
+ padding: 2rem 3rem;
+}
+
@media (max-width: 767.98px) {
.box {
padding: 0.75rem;
diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts
index 998beb1eb..49815fcb1 100644
--- a/unfurler/src/index.ts
+++ b/unfurler/src/index.ts
@@ -53,7 +53,7 @@ class Server {
}
async clusterTask({ page, data: { url, action } }) {
- await page.goto(url, { waitUntil: "domcontentloaded" });
+ await page.goto(url, { waitUntil: "networkidle0" });
switch (action) {
case 'screenshot': {
await page.evaluate(async () => {
@@ -73,11 +73,21 @@ class Server {
}),
]);
});
+ const waitForReady = await page.$('meta[property="og:loading"]');
+ const alreadyReady = await page.$('meta[property="og:ready"]');
+ if (waitForReady != null && alreadyReady == null) {
+ try {
+ await page.waitForSelector('meta[property="og:ready]"', { timeout: 10000 });
+ } catch (e) {
+ // probably timed out
+ }
+ }
return page.screenshot();
} break;
default: {
try {
- await page.waitForSelector('meta[property="og:title"', { timeout: 5000 })
+ await page.waitForSelector('meta[property="og:title"]', { timeout: 10000 })
+ const tag = await page.$('meta[property="og:title"]');
} catch (e) {
// probably timed out
}