2022-11-21 11:59:45 +09:00
import { Component , OnInit , Input , OnChanges , HostListener , Inject , LOCALE_ID } from '@angular/core' ;
2022-10-04 23:29:04 +00:00
import { StateService } from '../../services/state.service' ;
import { Outspend , Transaction } from '../../interfaces/electrs.interface' ;
import { Router } from '@angular/router' ;
2023-03-06 00:02:21 -06:00
import { ReplaySubject , merge , Subscription , of } from 'rxjs' ;
2022-10-04 23:29:04 +00:00
import { tap , switchMap } from 'rxjs/operators' ;
import { ApiService } from '../../services/api.service' ;
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe' ;
2022-08-22 19:12:04 +00:00
interface SvgLine {
path : string ;
style : string ;
class ? : string ;
2022-11-21 12:28:26 +09:00
connectorPath? : string ;
2022-11-21 12:28:46 +09:00
markerPath? : string ;
2022-11-20 11:32:03 +09:00
zeroValue? : boolean ;
2022-08-22 19:12:04 +00:00
}
2022-09-16 20:48:07 +00:00
interface Xput {
type : 'input' | 'output' | 'fee' ;
value? : number ;
2022-09-17 01:20:08 +00:00
index? : number ;
2022-11-22 10:05:14 +09:00
txid? : string ;
vin? : number ;
vout? : number ;
2022-09-17 01:20:08 +00:00
address? : string ;
rest? : number ;
2022-09-17 17:23:44 +00:00
coinbase? : boolean ;
pegin? : boolean ;
pegout? : string ;
confidential? : boolean ;
2023-02-23 10:46:18 +09:00
timestamp? : number ;
2022-09-16 20:48:07 +00:00
}
2022-08-22 19:12:04 +00:00
@Component ( {
selector : 'tx-bowtie-graph' ,
templateUrl : './tx-bowtie-graph.component.html' ,
styleUrls : [ './tx-bowtie-graph.component.scss' ] ,
} )
export class TxBowtieGraphComponent implements OnInit , OnChanges {
@Input ( ) tx : Transaction ;
2022-08-24 18:54:11 +00:00
@Input ( ) network : string ;
2023-03-06 00:02:21 -06:00
@Input ( ) cached : boolean = false ;
2022-08-22 19:12:04 +00:00
@Input ( ) width = 1200 ;
@Input ( ) height = 600 ;
2022-09-23 19:03:21 +00:00
@Input ( ) lineLimit = 250 ;
@Input ( ) maxCombinedWeight = 100 ;
2022-08-22 19:12:04 +00:00
@Input ( ) minWeight = 2 ; //
@Input ( ) maxStrands = 24 ; // number of inputs/outputs to keep fully on-screen.
2022-09-17 01:20:08 +00:00
@Input ( ) tooltip = false ;
2022-11-21 12:28:26 +09:00
@Input ( ) connectors = false ;
2022-10-04 23:30:14 +00:00
@Input ( ) inputIndex : number ;
@Input ( ) outputIndex : number ;
2022-08-22 19:12:04 +00:00
2022-11-21 11:59:45 +09:00
dir : 'rtl' | 'ltr' = 'ltr' ;
2022-09-17 01:20:08 +00:00
inputData : Xput [ ] ;
outputData : Xput [ ] ;
2022-08-22 19:12:04 +00:00
inputs : SvgLine [ ] ;
outputs : SvgLine [ ] ;
middle : SvgLine ;
2022-09-16 20:48:07 +00:00
midWidth : number ;
2022-11-22 10:05:14 +09:00
txWidth : number ;
connectorWidth : number ;
2022-09-23 19:03:21 +00:00
combinedWeight : number ;
2022-08-24 18:54:11 +00:00
isLiquid : boolean = false ;
2022-09-17 01:20:08 +00:00
hoverLine : Xput | void = null ;
2022-11-22 10:05:14 +09:00
hoverConnector : boolean = false ;
2022-09-17 01:20:08 +00:00
tooltipPosition = { x : 0 , y : 0 } ;
2022-10-04 23:29:04 +00:00
outspends : Outspend [ ] = [ ] ;
2022-11-20 11:32:03 +09:00
zeroValueWidth = 60 ;
zeroValueThickness = 20 ;
2022-12-06 17:45:04 -06:00
hasLine : boolean ;
2022-10-04 23:29:04 +00:00
outspendsSubscription : Subscription ;
refreshOutspends$ : ReplaySubject < string > = new ReplaySubject ( ) ;
2022-08-24 18:54:11 +00:00
gradientColors = {
2022-11-22 10:05:14 +09:00
'' : [ '#9339f4' , '#105fb0' , '#9339f400' ] ,
bisq : [ '#9339f4' , '#105fb0' , '#9339f400' ] ,
2022-08-24 18:54:11 +00:00
// liquid: ['#116761', '#183550'],
2022-11-22 10:05:14 +09:00
liquid : [ '#09a197' , '#0f62af' , '#09a19700' ] ,
2022-08-24 18:54:11 +00:00
// 'liquidtestnet': ['#494a4a', '#272e46'],
2022-11-22 10:05:14 +09:00
'liquidtestnet' : [ '#d2d2d2' , '#979797' , '#d2d2d200' ] ,
2022-08-24 18:54:11 +00:00
// testnet: ['#1d486f', '#183550'],
2022-11-22 10:05:14 +09:00
testnet : [ '#4edf77' , '#10a0af' , '#4edf7700' ] ,
2022-08-24 18:54:11 +00:00
// signet: ['#6f1d5d', '#471850'],
2022-11-22 10:05:14 +09:00
signet : [ '#d24fc8' , '#a84fd2' , '#d24fc800' ] ,
2022-08-24 18:54:11 +00:00
} ;
gradient : string [ ] = [ '#105fb0' , '#105fb0' ] ;
2022-08-22 19:12:04 +00:00
2022-10-04 23:29:04 +00:00
constructor (
private router : Router ,
private relativeUrlPipe : RelativeUrlPipe ,
private stateService : StateService ,
private apiService : ApiService ,
2022-11-21 11:59:45 +09:00
@Inject ( LOCALE_ID ) private locale : string ,
) {
if ( this . locale . startsWith ( 'ar' ) || this . locale . startsWith ( 'fa' ) || this . locale . startsWith ( 'he' ) ) {
this . dir = 'rtl' ;
}
}
2022-10-04 23:29:04 +00:00
2022-08-22 19:12:04 +00:00
ngOnInit ( ) : void {
this . initGraph ( ) ;
2022-10-04 23:29:04 +00:00
this . outspendsSubscription = merge (
this . refreshOutspends $
. pipe (
2023-03-06 00:02:21 -06:00
switchMap ( ( txid ) = > {
if ( ! this . cached ) {
return this . apiService . getOutspendsBatched $ ( [ txid ] ) ;
} else {
return of ( null ) ;
}
} ) ,
2022-10-04 23:29:04 +00:00
tap ( ( outspends : Outspend [ ] [ ] ) = > {
if ( ! this . tx || ! outspends || ! outspends . length ) {
return ;
}
this . outspends = outspends [ 0 ] ;
} ) ,
) ,
this . stateService . utxoSpent $
. pipe (
tap ( ( utxoSpent ) = > {
for ( const i in utxoSpent ) {
this . outspends [ i ] = {
spent : true ,
txid : utxoSpent [ i ] . txid ,
vin : utxoSpent [ i ] . vin ,
} ;
}
} ) ,
) ,
) . subscribe ( ( ) = > { } ) ;
2022-08-22 19:12:04 +00:00
}
ngOnChanges ( ) : void {
this . initGraph ( ) ;
2023-03-06 00:02:21 -06:00
if ( ! this . cached ) {
this . refreshOutspends $ . next ( this . tx . txid ) ;
}
2022-08-22 19:12:04 +00:00
}
initGraph ( ) : void {
2022-09-23 19:03:21 +00:00
this . isLiquid = ( this . network === 'liquid' || this . network === 'liquidtestnet' ) ;
this . gradient = this . gradientColors [ this . network ] ;
this . midWidth = Math . min ( 10 , Math . ceil ( this . width / 100 ) ) ;
2022-11-22 10:05:14 +09:00
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 ;
2022-11-20 11:32:03 +09:00
this . zeroValueWidth = Math . max ( 20 , Math . min ( ( this . txWidth / 2 ) - this . midWidth - 110 , 60 ) ) ;
2022-09-23 19:03:21 +00:00
2022-08-22 19:12:04 +00:00
const totalValue = this . calcTotalValue ( this . tx ) ;
2022-10-04 21:00:46 +00:00
let voutWithFee = this . tx . vout . map ( ( v , i ) = > {
2022-09-17 01:20:08 +00:00
return {
type : v . scriptpubkey_type === 'fee' ? 'fee' : 'output' ,
value : v?.value ,
address : v?.scriptpubkey_address || v ? . scriptpubkey_type ? . toUpperCase ( ) ,
2022-10-04 21:00:46 +00:00
index : i ,
2022-09-17 17:23:44 +00:00
pegout : v?.pegout?.scriptpubkey_address ,
confidential : ( this . isLiquid && v ? . value === undefined ) ,
2023-02-23 10:46:18 +09:00
timestamp : this.tx.status.block_time
2022-09-17 01:20:08 +00:00
} as Xput ;
} ) ;
2022-08-22 19:12:04 +00:00
if ( this . tx . fee && ! this . isLiquid ) {
voutWithFee . unshift ( { type : 'fee' , value : this.tx.fee } ) ;
}
2022-09-17 01:20:08 +00:00
const outputCount = voutWithFee . length ;
2022-08-22 19:12:04 +00:00
2022-10-04 21:00:46 +00:00
let truncatedInputs = this . tx . vin . map ( ( v , i ) = > {
2022-09-17 01:20:08 +00:00
return {
type : 'input' ,
2022-12-06 17:45:04 -06:00
value : v?.prevout?.value || ( v ? . is_coinbase && ! totalValue ? 0 : undefined ) ,
2022-11-22 10:05:14 +09:00
txid : v.txid ,
vout : v.vout ,
2022-09-17 01:20:08 +00:00
address : v?.prevout?.scriptpubkey_address || v ? . prevout ? . scriptpubkey_type ? . toUpperCase ( ) ,
2022-10-04 21:00:46 +00:00
index : i ,
2022-09-17 17:23:44 +00:00
coinbase : v?.is_coinbase ,
pegin : v?.is_pegin ,
confidential : ( this . isLiquid && v ? . prevout ? . value === undefined ) ,
2023-02-23 10:46:18 +09:00
timestamp : this.tx.status.block_time
2022-09-17 01:20:08 +00:00
} as Xput ;
} ) ;
2022-09-16 20:48:07 +00:00
2022-09-23 19:03:21 +00:00
if ( truncatedInputs . length > this . lineLimit ) {
const valueOfRest = truncatedInputs . slice ( this . lineLimit ) . reduce ( ( r , v ) = > {
2022-09-16 20:48:07 +00:00
return r + ( v . value || 0 ) ;
} , 0 ) ;
2022-09-23 19:03:21 +00:00
truncatedInputs = truncatedInputs . slice ( 0 , this . lineLimit ) ;
truncatedInputs . push ( { type : 'input' , value : valueOfRest , rest : this.tx.vin.length - this . lineLimit } ) ;
2022-09-16 20:48:07 +00:00
}
2022-09-23 19:03:21 +00:00
if ( voutWithFee . length > this . lineLimit ) {
const valueOfRest = voutWithFee . slice ( this . lineLimit ) . reduce ( ( r , v ) = > {
2022-09-16 20:48:07 +00:00
return r + ( v . value || 0 ) ;
} , 0 ) ;
2022-09-23 19:03:21 +00:00
voutWithFee = voutWithFee . slice ( 0 , this . lineLimit ) ;
voutWithFee . push ( { type : 'output' , value : valueOfRest , rest : outputCount - this . lineLimit } ) ;
2022-09-16 20:48:07 +00:00
}
2022-09-17 01:20:08 +00:00
this . inputData = truncatedInputs ;
this . outputData = voutWithFee ;
2022-09-16 20:48:07 +00:00
this . inputs = this . initLines ( 'in' , truncatedInputs , totalValue , this . maxStrands ) ;
2022-08-22 19:12:04 +00:00
this . outputs = this . initLines ( 'out' , voutWithFee , totalValue , this . maxStrands ) ;
this . middle = {
2023-02-23 18:19:47 +09:00
path : ` M ${ ( this . width / 2 ) - this . midWidth } ${ ( this . height / 2 ) + 0.5 } L ${ ( this . width / 2 ) + this . midWidth } ${ ( this . height / 2 ) + 0.5 } ` ,
style : ` stroke-width: ${ this . combinedWeight + 1 } ; stroke: ${ this . gradient [ 1 ] } `
2022-08-22 19:12:04 +00:00
} ;
2022-12-06 17:45:04 -06:00
this . hasLine = this . inputs . reduce ( ( line , put ) = > line || ! put . zeroValue , false )
&& this . outputs . reduce ( ( line , put ) = > line || ! put . zeroValue , false ) ;
2022-08-22 19:12:04 +00:00
}
calcTotalValue ( tx : Transaction ) : number {
const totalOutput = this . tx . vout . reduce ( ( acc , v ) = > ( v . value == null ? 0 : v.value ) + acc , 0 ) ;
// simple sum of outputs + fee for bitcoin
if ( ! this . isLiquid ) {
return this . tx . fee ? totalOutput + this . tx.fee : totalOutput ;
} else {
const totalInput = this . tx . vin . reduce ( ( acc , v ) = > ( v ? . prevout ? . value == null ? 0 : v.prevout.value ) + acc , 0 ) ;
const confidentialInputCount = this . tx . vin . reduce ( ( acc , v ) = > acc + ( v ? . prevout ? . value == null ? 1 : 0 ) , 0 ) ;
const confidentialOutputCount = this . tx . vout . reduce ( ( acc , v ) = > acc + ( v . value == null ? 1 : 0 ) , 0 ) ;
// if there are unknowns on both sides, the total is indeterminate, so we'll just fudge it
if ( confidentialInputCount && confidentialOutputCount ) {
const knownInputCount = ( tx . vin . length - confidentialInputCount ) || 1 ;
const knownOutputCount = ( tx . vout . length - confidentialOutputCount ) || 1 ;
// assume confidential inputs/outputs have the same average value as the known ones
const adjustedTotalInput = totalInput + ( ( totalInput / knownInputCount ) * confidentialInputCount ) ;
const adjustedTotalOutput = totalOutput + ( ( totalOutput / knownOutputCount ) * confidentialOutputCount ) ;
2022-09-06 17:03:41 +00:00
return Math . max ( adjustedTotalInput , adjustedTotalOutput ) ;
2022-08-22 19:12:04 +00:00
} else {
// otherwise knowing the actual total of one side suffices
2022-09-06 17:03:41 +00:00
return Math . max ( totalInput , totalOutput ) ;
2022-08-22 19:12:04 +00:00
}
}
}
2022-09-16 20:48:07 +00:00
initLines ( side : 'in' | 'out' , xputs : Xput [ ] , total : number , maxVisibleStrands : number ) : SvgLine [ ] {
2022-09-06 17:03:41 +00:00
if ( ! total ) {
2022-09-23 19:03:21 +00:00
const weights = xputs . map ( ( put ) = > this . combinedWeight / xputs . length ) ;
2022-09-06 17:03:41 +00:00
return this . linesFromWeights ( side , xputs , weights , maxVisibleStrands ) ;
} else {
let unknownCount = 0 ;
let unknownTotal = total ;
xputs . forEach ( put = > {
if ( put . value == null ) {
unknownCount ++ ;
} else {
unknownTotal -= put . value as number ;
}
} ) ;
const unknownShare = unknownTotal / unknownCount ;
// conceptual weights
2022-09-23 19:03:21 +00:00
const weights = xputs . map ( ( put ) = > this . combinedWeight * ( put . value == null ? unknownShare : put.value as number ) / total ) ;
2022-09-06 17:03:41 +00:00
return this . linesFromWeights ( side , xputs , weights , maxVisibleStrands ) ;
}
}
2022-08-22 19:12:04 +00:00
2022-09-23 19:03:21 +00:00
linesFromWeights ( side : 'in' | 'out' , xputs : Xput [ ] , weights : number [ ] , maxVisibleStrands : number ) : SvgLine [ ] {
2022-11-20 11:32:03 +09:00
const lineParams = weights . map ( ( w , i ) = > {
2022-09-23 19:03:21 +00:00
return {
weight : w ,
2023-02-23 18:19:47 +09:00
thickness : xputs [ i ] . value === 0 ? this . zeroValueThickness : Math.max ( this . minWeight - 1 , w ) + 1 ,
2022-09-23 19:03:21 +00:00
offset : 0 ,
innerY : 0 ,
outerY : 0 ,
} ;
} ) ;
2022-08-22 19:12:04 +00:00
const visibleStrands = Math . min ( maxVisibleStrands , xputs . length ) ;
2022-09-23 19:03:21 +00:00
const visibleWeight = lineParams . slice ( 0 , visibleStrands ) . reduce ( ( acc , v ) = > v . thickness + acc , 0 ) ;
2022-08-22 19:12:04 +00:00
const gaps = visibleStrands - 1 ;
2022-09-23 19:03:21 +00:00
// bounds of the middle segment
2022-08-22 19:12:04 +00:00
const innerTop = ( this . height / 2 ) - ( this . combinedWeight / 2 ) ;
2023-02-23 18:19:47 +09:00
const innerBottom = innerTop + this . combinedWeight ;
2022-08-22 19:12:04 +00:00
// tracks the visual bottom of the endpoints of the previous line
let lastOuter = 0 ;
let lastInner = innerTop ;
// gap between strands
2022-11-22 15:54:26 +09:00
const spacing = Math . max ( 4 , ( this . height - visibleWeight ) / gaps ) ;
2022-08-22 19:12:04 +00:00
2022-09-23 19:03:21 +00:00
// curve adjustments to prevent overlaps
let offset = 0 ;
let minOffset = 0 ;
let maxOffset = 0 ;
let lastWeight = 0 ;
let pad = 0 ;
lineParams . forEach ( ( line , i ) = > {
2022-11-20 11:32:03 +09:00
if ( xputs [ i ] . value === 0 ) {
2022-11-22 15:54:26 +09:00
line . outerY = lastOuter + ( this . zeroValueThickness / 2 ) ;
2022-12-06 17:45:04 -06:00
if ( xputs . length === 1 ) {
line . outerY = ( this . height / 2 ) ;
}
2022-11-20 11:32:03 +09:00
lastOuter += this . zeroValueThickness + spacing ;
return ;
}
2022-08-22 19:12:04 +00:00
// set the vertical position of the (center of the) outer side of the line
2022-09-23 19:03:21 +00:00
line . outerY = lastOuter + ( line . thickness / 2 ) ;
2023-02-23 18:19:47 +09:00
line . innerY = Math . min ( innerBottom + ( line . thickness / 2 ) , Math . max ( innerTop + ( line . thickness / 2 ) , lastInner + ( line . weight / 2 ) ) ) ;
2022-08-22 19:12:04 +00:00
// special case to center single input/outputs
if ( xputs . length === 1 ) {
2022-09-23 19:03:21 +00:00
line . outerY = ( this . height / 2 ) ;
2022-08-22 19:12:04 +00:00
}
2022-09-23 19:03:21 +00:00
lastOuter += line . thickness + spacing ;
lastInner += line . weight ;
// calculate conservative lower bound of the amount of horizontal offset
// required to prevent this line overlapping its neighbor
if ( this . tooltip || ! xputs [ i ] . rest ) {
2022-11-22 16:30:04 +09:00
const w = ( this . txWidth - Math . max ( lastWeight , line . weight ) - ( this . connectorWidth * 2 ) ) / 2 ; // approximate horizontal width of the curved section of the line
2022-09-23 19:03:21 +00:00
const y1 = line . outerY ;
const y2 = line . innerY ;
const t = ( lastWeight + line . weight ) / 2 ; // distance between center of this line and center of previous line
2022-08-22 19:12:04 +00:00
2022-09-23 19:03:21 +00:00
// slope of the inflection point of the bezier curve
const dx = 0.75 * w ;
const dy = 1.5 * ( y2 - y1 ) ;
const a = Math . atan2 ( dy , dx ) ;
// parallel curves should be separated by >=t at the inflection point to prevent overlap
// vertical offset is always = t, contributing tCos(a)
// horizontal offset h will contribute hSin(a)
// tCos(a) + hSin(a) >= t
// h >= t(1 - cos(a)) / sin(a)
if ( Math . sin ( a ) !== 0 ) {
// (absolute value clamped to t for sanity)
offset += Math . max ( Math . min ( t * ( 1 - Math . cos ( a ) ) / Math . sin ( a ) , t ) , - t ) ;
}
line . offset = offset ;
minOffset = Math . min ( minOffset , offset ) ;
maxOffset = Math . max ( maxOffset , offset ) ;
pad = Math . max ( pad , line . thickness / 2 ) ;
lastWeight = line . weight ;
} else {
// skip the offsets for consolidated lines in unfurls, since these *should* overlap a little
}
} ) ;
// normalize offsets
lineParams . forEach ( ( line ) = > {
line . offset -= minOffset ;
} ) ;
maxOffset -= minOffset ;
return lineParams . map ( ( line , i ) = > {
2022-11-20 11:32:03 +09:00
if ( xputs [ i ] . value === 0 ) {
return {
path : this.makeZeroValuePath ( side , line . outerY ) ,
style : this.makeStyle ( this . zeroValueThickness , xputs [ i ] . type ) ,
class : xputs [ i ] . type ,
zeroValue : true ,
} ;
} else {
return {
path : this.makePath ( side , line . outerY , line . innerY , line . thickness , line . offset , pad + maxOffset ) ,
style : this.makeStyle ( line . thickness , 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 ) ,
} ;
}
2022-09-23 19:03:21 +00:00
} ) ;
2022-08-22 19:12:04 +00:00
}
2022-09-23 19:03:21 +00:00
makePath ( side : 'in' | 'out' , outer : number , inner : number , weight : number , offset : number , pad : number ) : string {
2022-11-22 10:05:14 +09:00
const start = ( weight * 0.5 ) + this . connectorWidth ;
2022-11-22 16:30:04 +09:00
const curveStart = Math . max ( start + 5 , pad + this . connectorWidth - offset ) ;
2022-09-23 19:03:21 +00:00
const end = this . width / 2 - ( this . midWidth * 0.9 ) + 1 ;
const curveEnd = end - offset - 10 ;
const midpoint = ( curveStart + curveEnd ) / 2 ;
2022-08-24 18:54:11 +00:00
// correct for svg horizontal gradient bug
if ( Math . round ( outer ) === Math . round ( inner ) ) {
outer -= 1 ;
}
2022-09-23 19:03:21 +00:00
if ( side === 'in' ) {
return ` M ${ start } ${ outer } L ${ curveStart } ${ outer } C ${ midpoint } ${ outer } , ${ midpoint } ${ inner } , ${ curveEnd } ${ inner } L ${ end } ${ inner } ` ;
} else { // mirrored in y-axis for the right hand side
return ` M ${ this . width - start } ${ outer } L ${ this . width - curveStart } ${ outer } C ${ this . width - midpoint } ${ outer } , ${ this . width - midpoint } ${ inner } , ${ this . width - curveEnd } ${ inner } L ${ this . width - end } ${ inner } ` ;
}
2022-08-22 19:12:04 +00:00
}
2022-11-20 11:32:03 +09:00
makeZeroValuePath ( side : 'in' | 'out' , y : number ) : string {
const offset = this . zeroValueThickness / 2 ;
2022-11-22 15:54:26 +09:00
const start = ( this . connectorWidth / 2 ) + 10 ;
2022-11-20 11:32:03 +09:00
if ( side === 'in' ) {
2022-11-22 15:54:26 +09:00
return ` M ${ start + offset } ${ y } L ${ start + this . zeroValueWidth + offset } ${ y } ` ;
2022-11-20 11:32:03 +09:00
} else { // mirrored in y-axis for the right hand side
2022-11-22 15:54:26 +09:00
return ` M ${ this . width - start - offset } ${ y } L ${ this . width - start - this . zeroValueWidth - offset } ${ y } ` ;
2022-11-20 11:32:03 +09:00
}
}
2022-11-21 12:28:26 +09:00
makeConnectorPath ( side : 'in' | 'out' , y : number , inner , weight : number ) : string {
const halfWidth = weight * 0.5 ;
2022-11-22 10:05:14 +09:00
const offset = 10 ; //Math.max(2, halfWidth * 0.2);
const lineEnd = this . connectorWidth ;
2022-11-21 12:28:26 +09:00
// align with for svg horizontal gradient bug correction
if ( Math . round ( y ) === Math . round ( inner ) ) {
y -= 1 ;
}
if ( side === 'in' ) {
2022-11-22 10:05:14 +09:00
return ` M ${ lineEnd - offset } ${ y - halfWidth } L ${ halfWidth + lineEnd - offset } ${ y } L ${ lineEnd - offset } ${ y + halfWidth } L - ${ 10 } ${ y + halfWidth } L - ${ 10 } ${ y - halfWidth } ` ;
2022-11-21 12:28:26 +09:00
} else {
2022-11-22 10:05:14 +09:00
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 } ` ;
2022-11-21 12:28:26 +09:00
}
}
2022-11-21 12:28:46 +09:00
makeMarkerPath ( side : 'in' | 'out' , y : number , inner , weight : number ) : string {
const halfWidth = weight * 0.5 ;
2022-11-22 10:05:14 +09:00
const offset = 10 ; //Math.max(2, halfWidth * 0.2);
const lineEnd = this . connectorWidth ;
2022-11-21 12:28:46 +09:00
// align with for svg horizontal gradient bug correction
if ( Math . round ( y ) === Math . round ( inner ) ) {
y -= 1 ;
}
if ( side === 'in' ) {
2022-11-22 10:05:14 +09:00
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 } ` ;
2022-11-21 12:28:46 +09:00
} else {
2022-11-22 10:05:14 +09:00
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 } ` ;
2022-11-21 12:28:46 +09:00
}
}
2022-08-22 19:12:04 +00:00
makeStyle ( minWeight , type ) : string {
if ( type === 'fee' ) {
2022-09-17 01:20:08 +00:00
return ` stroke-width: ${ minWeight } ` ;
2022-08-22 19:12:04 +00:00
} else {
return ` stroke-width: ${ minWeight } ` ;
}
}
2022-09-17 01:20:08 +00:00
@HostListener ( 'pointermove' , [ '$event' ] )
onPointerMove ( event ) {
2022-11-21 11:59:45 +09:00
if ( this . dir === 'rtl' ) {
this . tooltipPosition = { x : this.width - event . offsetX , y : event.offsetY } ;
} else {
this . tooltipPosition = { x : event.offsetX , y : event.offsetY } ;
}
2022-09-17 01:20:08 +00:00
}
onHover ( event , side , index ) : void {
2022-11-22 10:05:14 +09:00
if ( side . startsWith ( 'input' ) ) {
2022-09-17 01:20:08 +00:00
this . hoverLine = {
. . . this . inputData [ index ] ,
index
} ;
2022-11-22 10:05:14 +09:00
this . hoverConnector = ( side === 'input-connector' ) ;
2022-09-17 01:20:08 +00:00
} else {
this . hoverLine = {
2022-11-22 10:05:14 +09:00
. . . this . outputData [ index ] ,
. . . this . outspends [ this . outputData [ index ] . index ]
2022-09-17 01:20:08 +00:00
} ;
2022-11-22 10:05:14 +09:00
this . hoverConnector = ( side === 'output-connector' ) ;
2022-09-17 01:20:08 +00:00
}
}
onBlur ( event , side , index ) : void {
this . hoverLine = null ;
2022-11-22 10:05:14 +09:00
this . hoverConnector = false ;
2022-09-17 01:20:08 +00:00
}
2022-10-04 21:00:46 +00:00
onClick ( event , side , index ) : void {
2022-11-22 10:05:14 +09:00
if ( side . startsWith ( 'input' ) ) {
2022-10-04 23:29:04 +00:00
const input = this . tx . vin [ index ] ;
2022-11-22 10:05:14 +09:00
if ( side === 'input-connector' && input && ! input . is_coinbase && ! input . is_pegin && input . txid && input . vout != null ) {
2022-10-11 20:54:17 +00:00
this . router . navigate ( [ this . relativeUrlPipe . transform ( '/tx' ) , input . txid ] , {
2022-10-04 23:29:04 +00:00
queryParamsHandling : 'merge' ,
2022-10-11 20:54:17 +00:00
fragment : ( new URLSearchParams ( {
flow : '' ,
vout : input.vout.toString ( ) ,
} ) ) . toString ( ) ,
} ) ;
} else if ( index != null ) {
this . router . navigate ( [ this . relativeUrlPipe . transform ( '/tx' ) , this . tx . txid ] , {
queryParamsHandling : 'merge' ,
fragment : ( new URLSearchParams ( {
flow : '' ,
vin : index.toString ( ) ,
} ) ) . toString ( ) ,
2022-10-04 23:29:04 +00:00
} ) ;
}
2022-10-04 21:00:46 +00:00
} else {
2022-10-04 23:29:04 +00:00
const output = this . tx . vout [ index ] ;
const outspend = this . outspends [ index ] ;
2022-11-22 10:05:14 +09:00
if ( side === 'output-connector' && output && outspend && outspend . spent && outspend . txid ) {
2022-10-11 20:54:17 +00:00
this . router . navigate ( [ this . relativeUrlPipe . transform ( '/tx' ) , outspend . txid ] , {
2022-10-04 23:29:04 +00:00
queryParamsHandling : 'merge' ,
2022-10-11 20:54:17 +00:00
fragment : ( new URLSearchParams ( {
flow : '' ,
vin : outspend.vin.toString ( ) ,
} ) ) . toString ( ) ,
} ) ;
} else if ( index != null ) {
this . router . navigate ( [ this . relativeUrlPipe . transform ( '/tx' ) , this . tx . txid ] , {
queryParamsHandling : 'merge' ,
fragment : ( new URLSearchParams ( {
flow : '' ,
vout : index.toString ( ) ,
} ) ) . toString ( ) ,
2022-10-04 23:29:04 +00:00
} ) ;
}
2022-10-04 21:00:46 +00:00
}
}
2022-08-22 19:12:04 +00:00
}