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' ;
2023-03-29 07:41:45 +09:00
import { Outspend , Transaction , Vin , Vout } from '../../interfaces/electrs.interface' ;
2022-10-04 23:29:04 +00:00
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' ;
2023-03-12 17:36:45 +09:00
import { AssetsService } from '../../services/assets.service' ;
2023-03-29 07:41:45 +09:00
import { environment } from '../../../environments/environment' ;
2023-08-16 02:25:48 +09:00
import { ElectrsApiService } from '../../services/electrs-api.service' ;
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 ;
2023-03-29 07:41:45 +09:00
displayValue? : 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 ;
2024-03-17 16:25:36 +09:00
blockHeight? : number ;
2023-03-12 17:36:45 +09:00
asset? : string ;
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 ;
2023-03-12 17:36:45 +09:00
assetsMinimal : any ;
2023-03-29 07:41:45 +09:00
nativeAssetId = this . stateService . network === 'liquidtestnet' ? environment.nativeTestAssetId : environment.nativeAssetId ;
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 ,
2023-08-16 02:25:48 +09:00
private electrsApiService : ElectrsApiService ,
2023-03-12 17:36:45 +09:00
private assetsService : AssetsService ,
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
2023-03-12 17:36:45 +09:00
if ( this . network === 'liquid' || this . network === 'liquidtestnet' ) {
this . assetsService . getAssetsMinimalJson$ . subscribe (( assets ) => {
this . assetsMinimal = assets ;
});
}
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 ) {
2023-11-12 06:38:18 +00:00
return this . electrsApiService . cachedRequest ( this . electrsApiService . getOutspendsBatched$ , 250 , [ txid ]);
2023-03-06 00:02:21 -06:00
} 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' ,
2023-03-29 07:41:45 +09:00
value : this.getOutputValue ( v ),
displayValue : v?.value ,
2022-09-17 01:20:08 +00:00
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-03-12 17:36:45 +09:00
timestamp : this.tx.status.block_time ,
2024-03-17 16:25:36 +09:00
blockHeight : this.tx.status.block_height ,
2023-03-12 17:36:45 +09:00
asset : v?.asset ,
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' ,
2023-03-29 07:41:45 +09:00
value : ( v ? . is_coinbase && ! totalValue ? 0 : this.getInputValue ( v )),
displayValue : v?.prevout?.value ,
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-03-12 17:36:45 +09:00
timestamp : this.tx.status.block_time ,
2024-03-17 16:25:36 +09:00
blockHeight : this.tx.status.block_height ,
2023-03-12 17:36:45 +09:00
asset : v?.prevout?.asset ,
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-21 21:59:31 -06:00
path : `M ${ ( this . width / 2 ) - this . midWidth } ${ ( this . height / 2 ) + 0.25 } L ${ ( this . width / 2 ) + this . midWidth } ${ ( this . height / 2 ) + 0.25 } ` ,
style : `stroke-width: ${ this . combinedWeight + 0.5 } ; 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 {
2023-03-29 07:41:45 +09:00
let totalOutput = this . tx . vout . reduce (( acc , v ) => ( this . getOutputValue ( v ) || 0 ) + acc , 0 );
2022-08-22 19:12:04 +00:00
// simple sum of outputs + fee for bitcoin
if ( ! this . isLiquid ) {
return this . tx . fee ? totalOutput + this . tx.fee : totalOutput ;
} else {
2023-03-29 07:41:45 +09:00
const totalInput = this . tx . vin . reduce (( acc , v ) => ( this . getInputValue ( v ) || 0 ) + acc , 0 );
const confidentialInputCount = this . tx . vin . reduce (( acc , v ) => acc + ( this . isUnknownInputValue ( v ) ? 1 : 0 ), 0 );
const confidentialOutputCount = this . tx . vout . reduce (( acc , v ) => acc + ( this . isUnknownOutputValue ( v ) ? 1 : 0 ), 0 );
2022-08-22 19:12:04 +00:00
// 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-21 21:59:31 -06:00
thickness : xputs [ i ]. value === 0 ? this . zeroValueThickness : Math.min ( this . combinedWeight + 0.5 , 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-21 21:59:31 -06:00
const innerBottom = innerTop + this . combinedWeight + 0.5 ;
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-21 21:59:31 -06: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
2023-03-29 07:41:45 +09:00
getOutputValue ( v : Vout ) : number | void {
if ( ! v ) {
return null ;
} else if ( this . isLiquid && v . asset !== this . nativeAssetId ) {
return null ;
} else {
return v . value ;
}
}
getInputValue ( v : Vin ) : number | void {
if ( ! v ? . prevout ) {
return null ;
} else if ( this . isLiquid && v . prevout . asset !== this . nativeAssetId ) {
return null ;
} else {
return v . prevout . value ;
}
}
isUnknownInputValue ( v : Vin ) : boolean {
return v ? . prevout ? . value == null || this . isLiquid && v ? . prevout ? . asset !== this . nativeAssetId ;
}
isUnknownOutputValue ( v : Vout ) : boolean {
return v ? . value == null || this . isLiquid && v ? . asset !== this . nativeAssetId ;
}
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
}