mirror of
https://github.com/mempool/mempool.git
synced 2025-04-23 23:10:45 +02:00
Merge pull request #5825 from mempool/natsoni/taptree
Taptree widget on addresss page
This commit is contained in:
commit
a941d52c91
@ -73,6 +73,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="hasTapTree">
|
||||
<br>
|
||||
<div class="title-tx">
|
||||
<h2 class="text-left" i18n="address.taproot-tree">Taproot Tree</h2>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="row">
|
||||
<div class="col-md">
|
||||
<app-taproot-address-scripts [address]="addressTypeInfo"></app-taproot-address-scripts>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="(stateService.backend$ | async) === 'esplora' && address && transactions && transactions.length > 2">
|
||||
<br>
|
||||
<div class="title-tx">
|
||||
@ -288,7 +302,7 @@
|
||||
<span placement="bottom" class="badge badge-primary">
|
||||
<app-address-type [address]="addressTypeInfo"></app-address-type>
|
||||
</span>
|
||||
<app-address-labels [channel]="exampleChannel" [address]="addressTypeInfo" class="ml-1"></app-address-labels>
|
||||
<app-address-labels *ngIf="!hasTapTree" [channel]="exampleChannel" [address]="addressTypeInfo" class="ml-1"></app-address-labels>
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
|
@ -115,6 +115,7 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
addressLoadingStatus$: Observable<number>;
|
||||
addressInfo: null | AddressInformation = null;
|
||||
addressTypeInfo: null | AddressTypeInfo;
|
||||
hasTapTree: boolean;
|
||||
|
||||
fullyLoaded = false;
|
||||
chainStats: AddressStats;
|
||||
@ -283,10 +284,17 @@ export class AddressComponent implements OnInit, OnDestroy {
|
||||
this.isLoadingTransactions = false;
|
||||
|
||||
let addressVin: Vin[] = [];
|
||||
let vinIds: string[] = [];
|
||||
for (const tx of this.transactions) {
|
||||
addressVin = addressVin.concat(tx.vin.filter(v => v.prevout?.scriptpubkey_address === this.address.address));
|
||||
tx.vin.forEach((v, index) => {
|
||||
if (v.prevout?.scriptpubkey_address === this.address.address) {
|
||||
addressVin.push(v);
|
||||
vinIds.push(`${tx.txid}:${index}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.addressTypeInfo.processInputs(addressVin);
|
||||
this.addressTypeInfo.processInputs(addressVin, vinIds);
|
||||
this.hasTapTree = this.addressTypeInfo.tapscript && this.addressTypeInfo.scripts.values().next().value.scriptPath.length / 2 > 33;
|
||||
// hack to trigger change detection
|
||||
this.addressTypeInfo = this.addressTypeInfo.clone();
|
||||
|
||||
|
@ -0,0 +1,20 @@
|
||||
<div>
|
||||
<div class="echarts" *browserOnly echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)" [style.height.px]="height"></div>
|
||||
<div class="text-center loadingGraphs" *ngIf="!stateService.isBrowser">
|
||||
<div class="spinner-border text-light"></div>
|
||||
</div>
|
||||
<div [class.fade-out]="stateService.isBrowser && !fullTreeShown && depthShown < depth"></div>
|
||||
<div class="toggle-wrapper" *ngIf="stateService.isBrowser && (depthShown < depth || fullTreeShown)">
|
||||
<button class="btn btn-sm btn-primary graph-toggle" (click)="toggleTree(true)" *ngIf="depthShown < depth && !fullTreeShown; else collapseBtn">
|
||||
<span i18n="show-all">Show all</span>
|
||||
(<ng-container *ngIf="(depth - depthShown) === 1"><ng-container *ngTemplateOutlet="xRemainingSingular"></ng-container></ng-container>
|
||||
<ng-container *ngIf="(depth - depthShown) > 1"><ng-container *ngTemplateOutlet="xRemaining; context: {$implicit: depth - depthShown}"></ng-container></ng-container>)
|
||||
</button>
|
||||
<ng-template #collapseBtn>
|
||||
<button class="btn btn-sm btn-primary graph-toggle" (click)="toggleTree(false)"><span i18n="show-less">Show less</span></button>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #xRemaining let-x i18n="x-levels-remaining">{{ x }} levels remaining</ng-template>
|
||||
<ng-template #xRemainingSingular let-x i18n="1-level-remaining">1 level remaining</ng-template>
|
@ -0,0 +1,31 @@
|
||||
.fade-out {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
top: -40px;
|
||||
background: linear-gradient(to top,
|
||||
var(--fade-out-box-bg-end) 0%,
|
||||
var(--fade-out-box-bg-end) 30%,
|
||||
var(--fade-out-box-bg-start) 100%);
|
||||
z-index: 10000000;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.graph-toggle {
|
||||
margin-top: 10px;
|
||||
}
|
@ -0,0 +1,395 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, OnChanges, NgZone, SimpleChanges, ChangeDetectorRef } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
import { AddressTypeInfo } from '@app/shared/address-utils';
|
||||
import { EChartsOption } from '@app/graphs/echarts';
|
||||
import { ScriptInfo } from '@app/shared/script.utils';
|
||||
import { compactSize, taggedHash, uint8ArrayToHexString } from '@app/shared/transaction.utils';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe';
|
||||
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
|
||||
|
||||
interface TaprootTree {
|
||||
name: string; // the TapBranch hash or TapLeaf script hash
|
||||
value?: LeafNode;
|
||||
depth?: number;
|
||||
children?: [TaprootTree, TaprootTree];
|
||||
// ECharts properties
|
||||
symbol?: string;
|
||||
symbolSize?: number;
|
||||
symbolOffset?: number[];
|
||||
label?: any;
|
||||
tooltip?: { label: string, content?: string }[];
|
||||
}
|
||||
|
||||
interface LeafNode {
|
||||
leafVersion: number;
|
||||
script: ScriptInfo;
|
||||
merklePath: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-taproot-address-scripts',
|
||||
templateUrl: './taproot-address-scripts.component.html',
|
||||
styleUrls: ['./taproot-address-scripts.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TaprootAddressScriptsComponent implements OnChanges {
|
||||
@Input() address: AddressTypeInfo;
|
||||
|
||||
tree: TaprootTree;
|
||||
croppedTree: TaprootTree;
|
||||
croppedTreeDepth: number = 7;
|
||||
depth: number = 0;
|
||||
depthShown: number;
|
||||
height: number;
|
||||
levelHeight: number = 40;
|
||||
fullTreeShown: boolean;
|
||||
|
||||
chartOptions: EChartsOption = {};
|
||||
chartInitOptions = {
|
||||
renderer: 'svg',
|
||||
};
|
||||
chartInstance: any;
|
||||
isTouchscreen: boolean = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || (navigator as any).msMaxTouchPoints > 0;
|
||||
|
||||
constructor(
|
||||
public stateService: StateService,
|
||||
private asmStylerPipe: AsmStylerPipe,
|
||||
private cd: ChangeDetectorRef,
|
||||
private location: Location,
|
||||
private relativeUrlPipe: RelativeUrlPipe,
|
||||
private router: Router,
|
||||
private zone: NgZone,
|
||||
) { }
|
||||
|
||||
ngOnChanges(changes: SimpleChanges) {
|
||||
if (changes.address?.currentValue.scripts && changes.address.currentValue.scripts.size) {
|
||||
this.buildTree(Array.from(this.address.scripts.values()));
|
||||
this.prepareTree(this.tree, 0);
|
||||
this.cropTree();
|
||||
this.toggleTree(this.fullTreeShown, false);
|
||||
}
|
||||
}
|
||||
|
||||
buildTree(scripts: ScriptInfo[]): void {
|
||||
// Parse script paths into merklePaths list and calculate depth
|
||||
const merklePaths: { leafVersion: number, merklePath: string[] }[] = [];
|
||||
this.depth = 0;
|
||||
for (const script of scripts) {
|
||||
const controlBlock = script.scriptPath;
|
||||
const m = ((controlBlock.length / 2) - 33) / 32;
|
||||
if (!Number.isInteger(m) || m <= 0) {
|
||||
throw new Error("Merkle path length must be >= 1");
|
||||
}
|
||||
const leafVersion = parseInt(controlBlock.slice(0, 2), 16) & 0xfe;
|
||||
const merklePath = [];
|
||||
for (let i = 0; i < m; i++) {
|
||||
merklePath.push(controlBlock.slice(66 + i * 64, 66 + (i + 1) * 64));
|
||||
}
|
||||
if (merklePath.length > this.depth) {
|
||||
this.depth = merklePath.length;
|
||||
}
|
||||
merklePaths.push({ leafVersion, merklePath });
|
||||
}
|
||||
|
||||
// treeStructure is a list of maps, where each map contains as keys the hashes of the nodes at that depth, and as values the hashes of its two children
|
||||
const treeStructure: Map<string, [string, string]>[] = [];
|
||||
for (let i = 0; i < this.depth; i++) {
|
||||
treeStructure.push(new Map<string, [string, string]>());
|
||||
}
|
||||
const leaves = new Map<string, LeafNode>();
|
||||
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
const script = scripts[i];
|
||||
const merklePath = merklePaths[i].merklePath;
|
||||
const leafVersion = merklePaths[i].leafVersion;
|
||||
const tapLeaf = taggedHash('TapLeaf', leafVersion.toString(16) + uint8ArrayToHexString(compactSize(script.hex.length / 2)) + script.hex);
|
||||
leaves.set(tapLeaf, { leafVersion, script, merklePath });
|
||||
let k = tapLeaf;
|
||||
for (let j = 0; j < merklePath.length; j++) {
|
||||
const e = merklePath[j];
|
||||
const [firstChild, secondChild] = [k, e].sort((a, b) => a.localeCompare(b));
|
||||
const parentHash = taggedHash('TapBranch', firstChild + secondChild);
|
||||
treeStructure[merklePath.length - j - 1].set(parentHash, [firstChild, secondChild]);
|
||||
k = parentHash;
|
||||
}
|
||||
}
|
||||
|
||||
// Build the tree recursively
|
||||
const recursiveBuild = (hash: string, depth: number): TaprootTree => {
|
||||
const node: TaprootTree = {
|
||||
name: hash,
|
||||
depth: depth
|
||||
};
|
||||
|
||||
if (leaves.has(hash)) {
|
||||
node.value = leaves.get(hash);
|
||||
return node;
|
||||
}
|
||||
|
||||
if (depth < treeStructure.length && treeStructure[depth].has(hash)) {
|
||||
const [firstChild, secondChild] = treeStructure[depth].get(hash);
|
||||
node.children = [
|
||||
recursiveBuild(firstChild, depth + 1),
|
||||
recursiveBuild(secondChild, depth + 1)
|
||||
];
|
||||
}
|
||||
|
||||
return node;
|
||||
};
|
||||
const root = treeStructure[0].keys().next().value;
|
||||
this.tree = recursiveBuild(root, 0);
|
||||
}
|
||||
|
||||
cropTree(): void {
|
||||
const cropNode = (node: TaprootTree, currentDepth: number) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
if (currentDepth === this.croppedTreeDepth && node.children) {
|
||||
delete node.children;
|
||||
return;
|
||||
}
|
||||
if (node.children) {
|
||||
cropNode(node.children[0], currentDepth + 1);
|
||||
cropNode(node.children[1], currentDepth + 1);
|
||||
}
|
||||
};
|
||||
this.croppedTree = JSON.parse(JSON.stringify(this.tree));
|
||||
cropNode(this.croppedTree, 0);
|
||||
}
|
||||
|
||||
toggleTree(show: boolean, delay = true): void {
|
||||
this.fullTreeShown = show;
|
||||
this.depthShown = show ? this.depth : Math.min(this.depth, this.croppedTreeDepth);
|
||||
if (show) {
|
||||
this.height = (this.depthShown + 1) * this.levelHeight;
|
||||
setTimeout(() => {
|
||||
this.prepareChartOptions(this.tree);
|
||||
this.cd.markForCheck();
|
||||
}, 115);
|
||||
} else {
|
||||
this.prepareChartOptions(this.croppedTree);
|
||||
if (!delay) {
|
||||
this.height = (this.depthShown + 1) * this.levelHeight;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.height = (this.depthShown + 1) * this.levelHeight;
|
||||
this.cd.markForCheck();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
prepareTree(node: TaprootTree, depth: number): void {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.depth = depth;
|
||||
node.symbol = 'none';
|
||||
|
||||
const basePillStyle = {
|
||||
align: 'center',
|
||||
padding: [3, 6],
|
||||
borderRadius: 10,
|
||||
fontSize: 10,
|
||||
fontWeight: 'bold',
|
||||
fontFamily: 'system-ui',
|
||||
};
|
||||
|
||||
if (depth === 0) {
|
||||
node.symbol = 'none';
|
||||
node.label = {
|
||||
formatter: '{pill|Taproot}',
|
||||
offset: [0, -5],
|
||||
rich: {
|
||||
pill: {
|
||||
...basePillStyle,
|
||||
backgroundColor: 'var(--tertiary)',
|
||||
color: '#fff',
|
||||
},
|
||||
},
|
||||
};
|
||||
node.tooltip = [
|
||||
{ label: 'TapRoot Hash', content: node.name.slice(0, 10) + '…' + node.name.slice(-10) },
|
||||
];
|
||||
}
|
||||
|
||||
if (node.children) {
|
||||
if (depth > 0) {
|
||||
node.symbol = 'circle';
|
||||
node.symbolSize = 10;
|
||||
node.symbolOffset = [0, 5];
|
||||
node.label = { formatter: '' };
|
||||
node.tooltip = [
|
||||
{ label: 'TapBranch Hash', content: node.name.slice(0, 10) + '…' + node.name.slice(-10) },
|
||||
{ label: 'Depth', content: depth.toString() },
|
||||
];
|
||||
}
|
||||
this.prepareTree(node.children[0], depth + 1);
|
||||
this.prepareTree(node.children[1], depth + 1);
|
||||
} else {
|
||||
if (node.value) {
|
||||
const script = node.value.script;
|
||||
const label = script.template?.label;
|
||||
|
||||
node.label = {
|
||||
formatter: `{pill|${label || 'Script'}}`,
|
||||
offset: [0, 5],
|
||||
verticalAlign: 'middle',
|
||||
rich: {
|
||||
pill: {
|
||||
...basePillStyle,
|
||||
backgroundColor: '#ffc107',
|
||||
color: '#212529'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
node.tooltip = [
|
||||
{ label: 'TapLeaf Hash', content: node.name.slice(0, 10) + '…' + node.name.slice(-10) },
|
||||
{ label: 'Depth', content: depth.toString() },
|
||||
{ label: 'Leaf Version', content: node.value.leafVersion.toString(16) },
|
||||
];
|
||||
|
||||
} else {
|
||||
node.symbol = 'circle';
|
||||
node.symbolSize = 10;
|
||||
node.symbolOffset = [0, 5];
|
||||
node.label = { formatter: '' };
|
||||
node.tooltip = [
|
||||
{ label: 'Hash', content: node.name.slice(0, 10) + '…' + node.name.slice(-10) },
|
||||
{ label: 'Depth', content: depth.toString() },
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prepareChartOptions(tree: TaprootTree) {
|
||||
if (!tree) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chartOptions = {
|
||||
tooltip: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(17, 19, 31, 1)',
|
||||
borderRadius: 4,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
confine: true,
|
||||
textStyle: {
|
||||
color: '#b1b1b1',
|
||||
},
|
||||
borderColor: '#000',
|
||||
formatter: (params: any) => {
|
||||
const node: TaprootTree = params.data;
|
||||
if (!node.tooltip) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let rows = node.tooltip.map(
|
||||
(item) =>
|
||||
`<tr>
|
||||
<td style="color: #fff; padding-right: 5px; width: 30%">${item.label}</td>
|
||||
<td style="color: #b1b1b1; text-align: right">${item.content}</td>
|
||||
</tr>`
|
||||
).join('');
|
||||
|
||||
if (node.value?.script.vinId) {
|
||||
const [txid, vinIndex] = node.value.script.vinId.split(':');
|
||||
rows += `
|
||||
<tr>
|
||||
<td style="color: #fff; padding-right: 5px; width: 30%">Last used in tx</td>
|
||||
<td style="color: #b1b1b1; text-align: right">
|
||||
<a href="${this.relativeUrlPipe.transform('/tx/' + txid)}?mode=details#vin=${vinIndex}">${txid.slice(0, 10) + '…' + txid.slice(-10)}</a>
|
||||
</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
let asmContent = '';
|
||||
if (node.value?.script?.asm) {
|
||||
const asm = this.asmStylerPipe.transform(node.value.script.asm, 300);
|
||||
asmContent = `
|
||||
<div style="margin-top: 10px; border-top: 1px solid #333; padding-top: 5px; word-break: break-all; white-space: normal; font-family: monospace; font-size: 12px;">
|
||||
<td>${asm} ${node.value.script.asm.length > 300 ? '...' : ''}</td>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
let hiddenScriptsMessage = '';
|
||||
if (node.tooltip[0].label === 'Hash') {
|
||||
hiddenScriptsMessage = `
|
||||
<div style="margin-top: 8px; color: #888; font-size: 11px; line-height: 1.3; font-style: italic; border-top: 1px solid #333; padding-top: 6px; word-break: break-word; white-space: normal">
|
||||
This node might commit to one or more scripts that have not been revealed yet.
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div style="max-width: 300px; pointer-events: auto;"">
|
||||
<table style="width: 100%; table-layout: fixed;">
|
||||
<tbody>${rows}</tbody>
|
||||
</table>
|
||||
${asmContent}
|
||||
${hiddenScriptsMessage}
|
||||
</div>`;
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
type: 'tree',
|
||||
data: [tree as any],
|
||||
top: '20',
|
||||
bottom: '20',
|
||||
right: 0,
|
||||
left: 0,
|
||||
height: Math.max(140, this.depthShown * this.levelHeight),
|
||||
lineStyle: {
|
||||
curveness: 0.9,
|
||||
width: 2,
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'ancestor',
|
||||
itemStyle: {
|
||||
color: '#ccc',
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#ccc',
|
||||
}
|
||||
},
|
||||
orient: 'TB',
|
||||
expandAndCollapse: false,
|
||||
animationDuration: 250,
|
||||
animationDurationUpdate: 250,
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
onChartInit(ec) {
|
||||
this.chartInstance = ec;
|
||||
this.chartInstance.on('click', 'series', this.onChartClick.bind(this));
|
||||
}
|
||||
|
||||
onChartClick(e): void {
|
||||
if (this.isTouchscreen) { // show tooltip on touchscreen, and click on link in tooltip to navigate
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.data.value?.script.vinId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [txid, vinIndex] = e.data.value.script.vinId.split(':');
|
||||
const url = this.router.createUrlTree([this.relativeUrlPipe.transform('/tx'), txid], { fragment: 'vin=' + vinIndex });
|
||||
|
||||
this.zone.run(() => {
|
||||
if (e.event?.event?.ctrlKey || e.event?.event?.metaKey) {
|
||||
const fullUrl = this.location.prepareExternalUrl(this.router.serializeUrl(url));
|
||||
window.open(fullUrl, '_blank');
|
||||
} else {
|
||||
this.router.navigate([this.relativeUrlPipe.transform('/tx'), txid], { fragment: 'vin=' + vinIndex });
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
@ -171,7 +171,7 @@
|
||||
<td i18n="transactions-list.p2wsh-witness-script">P2WSH witness script</td>
|
||||
</ng-template>
|
||||
<td style="text-align: left;">
|
||||
<div [innerHTML]="vin.inner_witnessscript_asm | asmStyler:showFullScript[vindex]"></div>
|
||||
<div [innerHTML]="vin.inner_witnessscript_asm | asmStyler: (showFullScript[vindex] ? 0 : 1000)"></div>
|
||||
<div *ngIf="vin.inner_witnessscript_asm.length > 1000" style="display: flex;">
|
||||
<span *ngIf="!showFullScript[vindex]">...</span>
|
||||
<label class="btn btn-sm btn-primary mt-2" (click)="toggleShowFullScript(vindex)" style="margin-left: auto;">
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Import tree-shakeable echarts
|
||||
import * as echarts from 'echarts/core';
|
||||
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart } from 'echarts/charts';
|
||||
import { LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart, CustomChart, TreeChart } from 'echarts/charts';
|
||||
import { TitleComponent, TooltipComponent, GridComponent, LegendComponent, GeoComponent, DataZoomComponent, VisualMapComponent, MarkLineComponent, GraphicComponent } from 'echarts/components';
|
||||
import { SVGRenderer, CanvasRenderer } from 'echarts/renderers';
|
||||
// Typescript interfaces
|
||||
@ -13,6 +13,6 @@ echarts.use([
|
||||
LegendComponent, GeoComponent, DataZoomComponent,
|
||||
VisualMapComponent, MarkLineComponent,
|
||||
LineChart, LinesChart, BarChart, TreemapChart, PieChart, ScatterChart, GaugeChart,
|
||||
CustomChart, GraphicComponent
|
||||
CustomChart, GraphicComponent, TreeChart
|
||||
]);
|
||||
export { echarts, EChartsOption, TreemapSeriesOption, LineSeriesOption, PieSeriesOption };
|
@ -40,7 +40,9 @@ import { AddressGraphComponent } from '@components/address-graph/address-graph.c
|
||||
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 { TaprootAddressScriptsComponent } from '@components/taproot-address-scripts/taproot-address-scripts.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { AsmStylerPipe } from '@app/shared/pipes/asm-styler/asm-styler.pipe';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
@ -83,6 +85,7 @@ import { CommonModule } from '@angular/common';
|
||||
UtxoGraphComponent,
|
||||
ActiveAccelerationBox,
|
||||
AddressesTreemap,
|
||||
TaprootAddressScriptsComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
@ -95,6 +98,9 @@ import { CommonModule } from '@angular/common';
|
||||
exports: [
|
||||
NgxEchartsModule,
|
||||
ActiveAccelerationBox,
|
||||
],
|
||||
providers: [
|
||||
AsmStylerPipe
|
||||
]
|
||||
})
|
||||
export class GraphsModule { }
|
||||
|
@ -152,14 +152,17 @@ export class AddressTypeInfo {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
public processInputs(vin: Vin[] = []): void {
|
||||
public processInputs(vin: Vin[] = [], vinIds: string[] = []): void {
|
||||
// taproot can have multiple script paths
|
||||
if (this.type === 'v1_p2tr') {
|
||||
for (const v of vin) {
|
||||
for (let i = 0; i < vin.length; i++) {
|
||||
const v = vin[i];
|
||||
if (v.inner_witnessscript_asm) {
|
||||
this.tapscript = true;
|
||||
const controlBlock = v.witness[v.witness.length - 1].startsWith('50') ? v.witness[v.witness.length - 2] : v.witness[v.witness.length - 1];
|
||||
this.processScript(new ScriptInfo('inner_witnessscript', undefined, v.inner_witnessscript_asm, v.witness, controlBlock));
|
||||
const hasAnnex = v.witness[v.witness.length - 1].startsWith('50');
|
||||
const controlBlock = hasAnnex ? v.witness[v.witness.length - 2] : v.witness[v.witness.length - 1];
|
||||
const scriptHex = hasAnnex ? v.witness[v.witness.length - 3] : v.witness[v.witness.length - 2];
|
||||
this.processScript(new ScriptInfo('inner_witnessscript', scriptHex, v.inner_witnessscript_asm, v.witness, controlBlock, vinIds?.[i]));
|
||||
}
|
||||
}
|
||||
// for single-script types, if we've seen one input we've seen them all
|
||||
@ -225,6 +228,9 @@ export class AddressTypeInfo {
|
||||
}
|
||||
|
||||
private processScript(script: ScriptInfo): void {
|
||||
if (this.scripts.has(script.key)) {
|
||||
return;
|
||||
}
|
||||
this.scripts.set(script.key, script);
|
||||
if (script.template?.type === 'multisig') {
|
||||
this.isMultisig = { m: script.template['m'], n: script.template['n'] };
|
||||
|
@ -5,7 +5,7 @@ import { Pipe, PipeTransform } from '@angular/core';
|
||||
})
|
||||
export class AsmStylerPipe implements PipeTransform {
|
||||
|
||||
transform(asm: string, showAll = true): string {
|
||||
transform(asm: string, crop: number = 0): string {
|
||||
const instructions = asm.split('OP_');
|
||||
let out = '';
|
||||
let chars = -3;
|
||||
@ -13,7 +13,7 @@ export class AsmStylerPipe implements PipeTransform {
|
||||
if (instruction === '') {
|
||||
continue;
|
||||
}
|
||||
if (!showAll && chars > 1000) {
|
||||
if (crop && chars > crop) {
|
||||
break;
|
||||
}
|
||||
chars += instruction.length + 3;
|
||||
|
@ -174,15 +174,19 @@ export class ScriptInfo {
|
||||
scriptPath?: string;
|
||||
hex?: string;
|
||||
asm?: string;
|
||||
vinId?: string;
|
||||
template: ScriptTemplate;
|
||||
|
||||
constructor(type: ScriptType, hex?: string, asm?: string, witness?: string[], scriptPath?: string) {
|
||||
constructor(type: ScriptType, hex?: string, asm?: string, witness?: string[], scriptPath?: string, vinId?: string) {
|
||||
this.type = type;
|
||||
this.hex = hex;
|
||||
this.asm = asm;
|
||||
if (scriptPath) {
|
||||
this.scriptPath = scriptPath;
|
||||
}
|
||||
if (vinId) {
|
||||
this.vinId = vinId;
|
||||
}
|
||||
if (this.asm) {
|
||||
this.template = detectScriptTemplate(this.type, this.asm, witness);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { getVarIntLength, parseMultisigScript, isPoint } from '@app/shared/scrip
|
||||
import { Transaction, Vin } from '@interfaces/electrs.interface';
|
||||
import { CpfpInfo, RbfInfo, TransactionStripped } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { Hash } from './sha256';
|
||||
import { hash, Hash } from './sha256';
|
||||
|
||||
// Bitcoin Core default policy settings
|
||||
const MAX_STANDARD_TX_WEIGHT = 400_000;
|
||||
@ -1380,11 +1380,11 @@ function toWords(bytes) {
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function uint8ArrayToHexString(uint8Array: Uint8Array): string {
|
||||
export function uint8ArrayToHexString(uint8Array: Uint8Array): string {
|
||||
return Array.from(uint8Array).map(byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
function hexStringToUint8Array(hex: string): Uint8Array {
|
||||
export function hexStringToUint8Array(hex: string): Uint8Array {
|
||||
const buf = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
buf[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
@ -1521,6 +1521,32 @@ function readVector(buffer: Uint8Array, offset: number): [Uint8Array[], number]
|
||||
return [vector, updatedOffset];
|
||||
}
|
||||
|
||||
// SHA256(SHA256(tag) || SHA256(tag) || dataHex)
|
||||
export function taggedHash(tag: string, dataHex: string): string {
|
||||
const encoder = new TextEncoder();
|
||||
const tagHash = hash(encoder.encode(tag));
|
||||
return uint8ArrayToHexString(hash(new Uint8Array([...tagHash, ...tagHash, ...hexStringToUint8Array(dataHex)])));
|
||||
}
|
||||
|
||||
export function compactSize(n: number): Uint8Array {
|
||||
if (n <= 252) {
|
||||
return new Uint8Array([n]);
|
||||
} else if (n <= 0xffff) {
|
||||
return new Uint8Array([0xfd, n & 0xff, (n >> 8) & 0xff]);
|
||||
} else if (n <= 0xffffffff) {
|
||||
return new Uint8Array([0xfe, n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >> 24) & 0xff]);
|
||||
} else {
|
||||
const buffer = new Uint8Array(9);
|
||||
buffer[0] = 0xff;
|
||||
let num = BigInt(n);
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
buffer[i] = Number(num & BigInt(0xff));
|
||||
num >>= BigInt(8);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
// Inversed the opcodes object from https://github.com/mempool/mempool/blob/14e49126c3ca8416a8d7ad134a95c5e090324d69/backend/src/utils/bitcoin-script.ts#L1
|
||||
const opcodes = {
|
||||
0: 'OP_0',
|
||||
|
Loading…
x
Reference in New Issue
Block a user