Merge pull request #2649 from mononaut/flow-diagram-spent-connectors

Extend flow diagram to differentiate spent and unspent TXOs
This commit is contained in:
wiz 2022-11-22 13:52:22 +09:00 committed by GitHub
commit 9345b1609f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 188 additions and 38 deletions

View File

@ -126,9 +126,13 @@ export class LiquidUnblinding {
} }
async checkUnblindedTx(tx: Transaction) { async checkUnblindedTx(tx: Transaction) {
const windowLocationHash = window.location.hash.substring('#blinded='.length); if (!window.location.hash?.length) {
if (windowLocationHash.length > 0) { return tx;
const blinders = this.parseBlinders(windowLocationHash); }
const fragmentParams = new URLSearchParams(window.location.hash.slice(1) || '');
const blinderStr = fragmentParams.get('blinded');
if (blinderStr && blinderStr.length) {
const blinders = this.parseBlinders(blinderStr);
if (blinders) { if (blinders) {
this.commitments = await this.makeCommitmentMap(blinders); this.commitments = await this.makeCommitmentMap(blinders);
return this.tryUnblindTx(tx); return this.tryUnblindTx(tx);

View File

@ -29,7 +29,7 @@
<div class="row graph-wrapper"> <div class="row graph-wrapper">
<tx-bowtie-graph [tx]="tx" [width]="1112" [height]="346" [network]="network"></tx-bowtie-graph> <tx-bowtie-graph [tx]="tx" [width]="1132" [height]="346" [network]="network"></tx-bowtie-graph>
<div class="above-bow"> <div class="above-bow">
<p class="field pair"> <p class="field pair">
<span [innerHTML]="'&lrm;' + (tx.size | bytes: 2)"></span> <span [innerHTML]="'&lrm;' + (tx.size | bytes: 2)"></span>

View File

@ -69,7 +69,7 @@
.graph-wrapper { .graph-wrapper {
position: relative; position: relative;
background: #181b2d; background: #181b2d;
padding: 10px; padding: 10px 0;
padding-bottom: 0; padding-bottom: 0;
.above-bow { .above-bow {

View File

@ -209,6 +209,7 @@
[maxStrands]="graphExpanded ? maxInOut : 24" [maxStrands]="graphExpanded ? maxInOut : 24"
[network]="network" [network]="network"
[tooltip]="true" [tooltip]="true"
[connectors]="true"
[inputIndex]="inputIndex" [outputIndex]="outputIndex" [inputIndex]="inputIndex" [outputIndex]="outputIndex"
> >
</tx-bowtie-graph> </tx-bowtie-graph>

View File

@ -86,7 +86,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
background: #181b2d; background: #181b2d;
padding: 10px; padding: 10px 0;
padding-bottom: 0; padding-bottom: 0;
} }

View File

@ -404,7 +404,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
setGraphSize(): void { setGraphSize(): void {
if (this.graphContainer) { if (this.graphContainer) {
this.graphWidth = this.graphContainer.nativeElement.clientWidth - 24; this.graphWidth = this.graphContainer.nativeElement.clientWidth;
} }
} }

View File

@ -22,13 +22,13 @@
<ng-template #pegin> <ng-template #pegin>
<ng-container *ngIf="line.pegin; else pegout"> <ng-container *ngIf="line.pegin; else pegout">
<p>Peg In</p> <p *ngIf="!isConnector">Peg In</p>
</ng-container> </ng-container>
</ng-template> </ng-template>
<ng-template #pegout> <ng-template #pegout>
<ng-container *ngIf="line.pegout; else normal"> <ng-container *ngIf="line.pegout; else normal">
<p>Peg Out</p> <p *ngIf="!isConnector">Peg Out</p>
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p> <p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
<p class="address"> <p class="address">
<span class="first">{{ line.pegout.slice(0, -4) }}</span> <span class="first">{{ line.pegout.slice(0, -4) }}</span>
@ -38,7 +38,7 @@
</ng-template> </ng-template>
<ng-template #normal> <ng-template #normal>
<p> <p *ngIf="!isConnector">
<ng-container [ngSwitch]="line.type"> <ng-container [ngSwitch]="line.type">
<span *ngSwitchCase="'input'" i18n="transaction.input">Input</span> <span *ngSwitchCase="'input'" i18n="transaction.input">Input</span>
<span *ngSwitchCase="'output'" i18n="transaction.output">Output</span> <span *ngSwitchCase="'output'" i18n="transaction.output">Output</span>
@ -46,6 +46,17 @@
</ng-container> </ng-container>
<span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span> <span *ngIf="line.type !== 'fee'"> #{{ line.index + 1 }}</span>
</p> </p>
<ng-container *ngIf="isConnector && line.txid">
<p>
<span i18n="transaction">Transaction</span>&nbsp;
<span class="first">{{ line.txid.slice(0, 8) }}</span>...
<span class="last-four">{{ line.txid.slice(-4) }}</span>
</p>
<ng-container [ngSwitch]="line.type">
<p *ngSwitchCase="'input'"><span i18n="transaction.output">Output</span>&nbsp; #{{ line.vout + 1 }}</p>
<p *ngSwitchCase="'output'"><span i18n="transaction.input">Input</span>&nbsp; #{{ line.vin + 1 }}</p>
</ng-container>
</ng-container>
<p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p> <p *ngIf="line.value == null && line.confidential" i18n="shared.confidential">Confidential</p>
<p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p> <p *ngIf="line.value != null"><app-amount [satoshis]="line.value"></app-amount></p>
<p *ngIf="line.type !== 'fee' && line.address" class="address"> <p *ngIf="line.type !== 'fee' && line.address" class="address">

View File

@ -5,6 +5,9 @@ interface Xput {
type: 'input' | 'output' | 'fee'; type: 'input' | 'output' | 'fee';
value?: number; value?: number;
index?: number; index?: number;
txid?: string;
vin?: number;
vout?: number;
address?: string; address?: string;
rest?: number; rest?: number;
coinbase?: boolean; coinbase?: boolean;
@ -21,6 +24,7 @@ interface Xput {
export class TxBowtieGraphTooltipComponent implements OnChanges { export class TxBowtieGraphTooltipComponent implements OnChanges {
@Input() line: Xput | void; @Input() line: Xput | void;
@Input() cursorPosition: { x: number, y: number }; @Input() cursorPosition: { x: number, y: number };
@Input() isConnector: boolean = false;
tooltipPosition = { x: 0, y: 0 }; tooltipPosition = { x: 0, y: 0 };

View File

@ -29,6 +29,14 @@
<stop offset="0%" [attr.stop-color]="gradient[1]" /> <stop offset="0%" [attr.stop-color]="gradient[1]" />
<stop offset="100%" [attr.stop-color]="gradient[0]" /> <stop offset="100%" [attr.stop-color]="gradient[0]" />
</linearGradient> </linearGradient>
<linearGradient id="input-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[2]" />
<stop offset="80%" [attr.stop-color]="gradient[0]" />
</linearGradient>
<linearGradient id="output-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="20%" [attr.stop-color]="gradient[0]" />
<stop offset="100%" [attr.stop-color]="gradient[2]" />
</linearGradient>
<linearGradient id="input-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> <linearGradient id="input-hover-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[0]" /> <stop offset="0%" [attr.stop-color]="gradient[0]" />
<stop offset="2%" [attr.stop-color]="gradient[0]" /> <stop offset="2%" [attr.stop-color]="gradient[0]" />
@ -41,6 +49,14 @@
<stop offset="98%" [attr.stop-color]="gradient[0]" /> <stop offset="98%" [attr.stop-color]="gradient[0]" />
<stop offset="100%" [attr.stop-color]="gradient[0]" /> <stop offset="100%" [attr.stop-color]="gradient[0]" />
</linearGradient> </linearGradient>
<linearGradient id="input-hover-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="white" />
<stop offset="80%" [attr.stop-color]="gradient[0]" />
</linearGradient>
<linearGradient id="output-hover-connector-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="20%" [attr.stop-color]="gradient[0]" />
<stop offset="100%" stop-color="white" />
</linearGradient>
<linearGradient id="input-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> <linearGradient id="input-highlight-gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" [attr.stop-color]="gradient[0]" /> <stop offset="0%" [attr.stop-color]="gradient[0]" />
<stop offset="2%" [attr.stop-color]="gradient[0]" /> <stop offset="2%" [attr.stop-color]="gradient[0]" />
@ -65,6 +81,22 @@
</defs> </defs>
<path [attr.d]="middle.path" class="line middle" [style]="middle.style"/> <path [attr.d]="middle.path" class="line middle" [style]="middle.style"/>
<ng-container *ngFor="let input of inputs; let i = index"> <ng-container *ngFor="let input of inputs; let i = index">
<path *ngIf="connectors && !inputData[i].coinbase && !inputData[i].pegin"
[attr.d]="input.connectorPath"
class="input connector {{input.class}}"
[class.highlight]="inputData[i].index === inputIndex"
(pointerover)="onHover($event, 'input-connector', i);"
(pointerout)="onBlur($event, 'input-connector', i);"
(click)="onClick($event, 'input-connector', inputData[i].index);"
/>
<path
[attr.d]="input.markerPath"
class="input marker-target {{input.class}}"
[class.highlight]="inputData[i].index === inputIndex"
(pointerover)="onHover($event, 'input', i);"
(pointerout)="onBlur($event, 'input', i);"
(click)="onClick($event, 'input', inputData[i].index);"
/>
<path <path
[attr.d]="input.path" [attr.d]="input.path"
class="line {{input.class}}" class="line {{input.class}}"
@ -77,6 +109,22 @@
/> />
</ng-container> </ng-container>
<ng-container *ngFor="let output of outputs; let i = index"> <ng-container *ngFor="let output of outputs; let i = index">
<path *ngIf="connectors && outspends[outputData[i].index]?.spent"
[attr.d]="output.connectorPath"
class="output connector {{output.class}}"
[class.highlight]="outputData[i].index === outputIndex"
(pointerover)="onHover($event, 'output-connector', i);"
(pointerout)="onBlur($event, 'output-connector', i);"
(click)="onClick($event, 'output-connector', outputData[i].index);"
/>
<path
[attr.d]="output.markerPath"
class="output marker-target {{output.class}}"
[class.highlight]="outputData[i].index === outputIndex"
(pointerover)="onHover($event, 'output', i);"
(pointerout)="onBlur($event, 'output', i);"
(click)="onClick($event, 'output', outputData[i].index);"
/>
<path <path
[attr.d]="output.path" [attr.d]="output.path"
class="line {{output.class}}" class="line {{output.class}}"
@ -94,5 +142,6 @@
*ngIf=[tooltip] *ngIf=[tooltip]
[line]="hoverLine" [line]="hoverLine"
[cursorPosition]="tooltipPosition" [cursorPosition]="tooltipPosition"
[isConnector]="hoverConnector"
></app-tx-bowtie-graph-tooltip> ></app-tx-bowtie-graph-tooltip>
</div> </div>

View File

@ -22,19 +22,46 @@
stroke: url(#output-highlight-gradient); stroke: url(#output-highlight-gradient);
} }
} }
}
&:hover { .line:hover, .marker-target:hover + .line {
z-index: 10; z-index: 10;
cursor: pointer; cursor: pointer;
&.input { &.input {
stroke: url(#input-hover-gradient); stroke: url(#input-hover-gradient);
} }
&.output { &.output {
stroke: url(#output-hover-gradient); stroke: url(#output-hover-gradient);
} }
&.fee { &.fee {
stroke: url(#fee-hover-gradient); stroke: url(#fee-hover-gradient);
}
} }
} }
}
.connector {
stroke: none;
opacity: 0.75;
cursor: pointer;
&.input {
fill: url(#input-connector-gradient);
}
&.output {
fill: url(#output-connector-gradient);
}
}
.connector:hover {
&.input {
fill: url(#input-hover-connector-gradient);
}
&.output {
fill: url(#output-hover-connector-gradient);
}
}
.marker-target {
stroke: none;
fill: transparent;
cursor: pointer;
}
}

View File

@ -11,12 +11,17 @@ interface SvgLine {
path: string; path: string;
style: string; style: string;
class?: string; class?: string;
connectorPath?: string;
markerPath?: string;
} }
interface Xput { interface Xput {
type: 'input' | 'output' | 'fee'; type: 'input' | 'output' | 'fee';
value?: number; value?: number;
index?: number; index?: number;
txid?: string;
vin?: number;
vout?: number;
address?: string; address?: string;
rest?: number; rest?: number;
coinbase?: boolean; coinbase?: boolean;
@ -40,6 +45,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
@Input() minWeight = 2; // @Input() minWeight = 2; //
@Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen. @Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen.
@Input() tooltip = false; @Input() tooltip = false;
@Input() connectors = false;
@Input() inputIndex: number; @Input() inputIndex: number;
@Input() outputIndex: number; @Input() outputIndex: number;
@ -49,9 +55,12 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
outputs: SvgLine[]; outputs: SvgLine[];
middle: SvgLine; middle: SvgLine;
midWidth: number; midWidth: number;
txWidth: number;
connectorWidth: number;
combinedWeight: number; combinedWeight: number;
isLiquid: boolean = false; isLiquid: boolean = false;
hoverLine: Xput | void = null; hoverLine: Xput | void = null;
hoverConnector: boolean = false;
tooltipPosition = { x: 0, y: 0 }; tooltipPosition = { x: 0, y: 0 };
outspends: Outspend[] = []; outspends: Outspend[] = [];
@ -59,16 +68,16 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
refreshOutspends$: ReplaySubject<string> = new ReplaySubject(); refreshOutspends$: ReplaySubject<string> = new ReplaySubject();
gradientColors = { gradientColors = {
'': ['#9339f4', '#105fb0'], '': ['#9339f4', '#105fb0', '#9339f400'],
bisq: ['#9339f4', '#105fb0'], bisq: ['#9339f4', '#105fb0', '#9339f400'],
// liquid: ['#116761', '#183550'], // liquid: ['#116761', '#183550'],
liquid: ['#09a197', '#0f62af'], liquid: ['#09a197', '#0f62af', '#09a19700'],
// 'liquidtestnet': ['#494a4a', '#272e46'], // 'liquidtestnet': ['#494a4a', '#272e46'],
'liquidtestnet': ['#d2d2d2', '#979797'], 'liquidtestnet': ['#d2d2d2', '#979797', '#d2d2d200'],
// testnet: ['#1d486f', '#183550'], // testnet: ['#1d486f', '#183550'],
testnet: ['#4edf77', '#10a0af'], testnet: ['#4edf77', '#10a0af', '#4edf7700'],
// signet: ['#6f1d5d', '#471850'], // signet: ['#6f1d5d', '#471850'],
signet: ['#d24fc8', '#a84fd2'], signet: ['#d24fc8', '#a84fd2', '#d24fc800'],
}; };
gradient: string[] = ['#105fb0', '#105fb0']; gradient: string[] = ['#105fb0', '#105fb0'];
@ -118,7 +127,9 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet'); this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet');
this.gradient = this.gradientColors[this.network]; this.gradient = this.gradientColors[this.network];
this.midWidth = Math.min(10, Math.ceil(this.width / 100)); this.midWidth = Math.min(10, Math.ceil(this.width / 100));
this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6)); this.txWidth = this.connectors ? Math.max(this.width - 200, this.width * 0.8) : this.width - 20;
this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.txWidth - (2 * this.midWidth)) / 6));
this.connectorWidth = (this.width - this.txWidth) / 2;
const totalValue = this.calcTotalValue(this.tx); const totalValue = this.calcTotalValue(this.tx);
let voutWithFee = this.tx.vout.map((v, i) => { let voutWithFee = this.tx.vout.map((v, i) => {
@ -141,6 +152,8 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
return { return {
type: 'input', type: 'input',
value: v?.prevout?.value, value: v?.prevout?.value,
txid: v.txid,
vout: v.vout,
address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(), address: v?.prevout?.scriptpubkey_address || v?.prevout?.scriptpubkey_type?.toUpperCase(),
index: i, index: i,
coinbase: v?.is_coinbase, coinbase: v?.is_coinbase,
@ -268,7 +281,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
// required to prevent this line overlapping its neighbor // required to prevent this line overlapping its neighbor
if (this.tooltip || !xputs[i].rest) { if (this.tooltip || !xputs[i].rest) {
const w = (this.width - Math.max(lastWeight, line.weight)) / 2; // approximate horizontal width of the curved section of the line const w = (this.width - Math.max(lastWeight, line.weight) - (2 * this.connectorWidth)) / 2; // approximate horizontal width of the curved section of the line
const y1 = line.outerY; const y1 = line.outerY;
const y2 = line.innerY; const y2 = line.innerY;
const t = (lastWeight + line.weight) / 2; // distance between center of this line and center of previous line const t = (lastWeight + line.weight) / 2; // distance between center of this line and center of previous line
@ -308,13 +321,15 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
return { return {
path: this.makePath(side, line.outerY, line.innerY, line.thickness, line.offset, pad + maxOffset), path: this.makePath(side, line.outerY, line.innerY, line.thickness, line.offset, pad + maxOffset),
style: this.makeStyle(line.thickness, xputs[i].type), style: this.makeStyle(line.thickness, xputs[i].type),
class: xputs[i].type class: xputs[i].type,
connectorPath: this.connectors ? this.makeConnectorPath(side, line.outerY, line.innerY, line.thickness): null,
markerPath: this.makeMarkerPath(side, line.outerY, line.innerY, line.thickness),
}; };
}); });
} }
makePath(side: 'in' | 'out', outer: number, inner: number, weight: number, offset: number, pad: number): string { makePath(side: 'in' | 'out', outer: number, inner: number, weight: number, offset: number, pad: number): string {
const start = (weight * 0.5); const start = (weight * 0.5) + this.connectorWidth;
const curveStart = Math.max(start + 1, pad - offset); const curveStart = Math.max(start + 1, pad - offset);
const end = this.width / 2 - (this.midWidth * 0.9) + 1; const end = this.width / 2 - (this.midWidth * 0.9) + 1;
const curveEnd = end - offset - 10; const curveEnd = end - offset - 10;
@ -332,6 +347,40 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
} }
} }
makeConnectorPath(side: 'in' | 'out', y: number, inner, weight: number): string {
const halfWidth = weight * 0.5;
const offset = 10; //Math.max(2, halfWidth * 0.2);
const lineEnd = this.connectorWidth;
// align with for svg horizontal gradient bug correction
if (Math.round(y) === Math.round(inner)) {
y -= 1;
}
if (side === 'in') {
return `M ${lineEnd - offset} ${y - halfWidth} L ${halfWidth + lineEnd - offset} ${y} L ${lineEnd - offset} ${y + halfWidth} L -${10} ${ y + halfWidth} L -${10} ${y - halfWidth}`;
} else {
return `M ${this.width - halfWidth - lineEnd + offset} ${y - halfWidth} L ${this.width - lineEnd + offset} ${y} L ${this.width - halfWidth - lineEnd + offset} ${y + halfWidth} L ${this.width + 10} ${ y + halfWidth} L ${this.width + 10} ${y - halfWidth}`;
}
}
makeMarkerPath(side: 'in' | 'out', y: number, inner, weight: number): string {
const halfWidth = weight * 0.5;
const offset = 10; //Math.max(2, halfWidth * 0.2);
const lineEnd = this.connectorWidth;
// align with for svg horizontal gradient bug correction
if (Math.round(y) === Math.round(inner)) {
y -= 1;
}
if (side === 'in') {
return `M ${lineEnd - offset} ${y - halfWidth} L ${halfWidth + lineEnd - offset} ${y} L ${lineEnd - offset} ${y + halfWidth} L ${weight + lineEnd} ${ y + halfWidth} L ${weight + lineEnd} ${y - halfWidth}`;
} else {
return `M ${this.width - halfWidth - lineEnd + offset} ${y - halfWidth} L ${this.width - lineEnd + offset} ${y} L ${this.width - halfWidth - lineEnd + offset} ${y + halfWidth} L ${this.width - halfWidth - lineEnd} ${ y + halfWidth} L ${this.width - halfWidth - lineEnd} ${y - halfWidth}`;
}
}
makeStyle(minWeight, type): string { makeStyle(minWeight, type): string {
if (type === 'fee') { if (type === 'fee') {
return `stroke-width: ${minWeight}`; return `stroke-width: ${minWeight}`;
@ -346,26 +395,31 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
} }
onHover(event, side, index): void { onHover(event, side, index): void {
if (side === 'input') { if (side.startsWith('input')) {
this.hoverLine = { this.hoverLine = {
...this.inputData[index], ...this.inputData[index],
index index
}; };
this.hoverConnector = (side === 'input-connector');
} else { } else {
this.hoverLine = { this.hoverLine = {
...this.outputData[index] ...this.outputData[index],
...this.outspends[this.outputData[index].index]
}; };
this.hoverConnector = (side === 'output-connector');
} }
} }
onBlur(event, side, index): void { onBlur(event, side, index): void {
this.hoverLine = null; this.hoverLine = null;
this.hoverConnector = false;
} }
onClick(event, side, index): void { onClick(event, side, index): void {
if (side === 'input') { if (side.startsWith('input')) {
const input = this.tx.vin[index]; const input = this.tx.vin[index];
if (input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) { if (side === 'input-connector' && input && !input.is_coinbase && !input.is_pegin && input.txid && input.vout != null) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], { this.router.navigate([this.relativeUrlPipe.transform('/tx'), input.txid], {
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
fragment: (new URLSearchParams({ fragment: (new URLSearchParams({
@ -385,7 +439,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges {
} else { } else {
const output = this.tx.vout[index]; const output = this.tx.vout[index];
const outspend = this.outspends[index]; const outspend = this.outspends[index];
if (output && outspend && outspend.spent && outspend.txid) { if (side === 'output-connector' && output && outspend && outspend.spent && outspend.txid) {
this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], { this.router.navigate([this.relativeUrlPipe.transform('/tx'), outspend.txid], {
queryParamsHandling: 'merge', queryParamsHandling: 'merge',
fragment: (new URLSearchParams({ fragment: (new URLSearchParams({