2022-08-15 23:14:34 +00:00
import { Component , OnInit , OnDestroy } from '@angular/core' ;
import { ElectrsApiService } from '../../services/electrs-api.service' ;
import { ActivatedRoute , ParamMap } from '@angular/router' ;
import {
switchMap ,
filter ,
catchError ,
retryWhen ,
delay ,
} from 'rxjs/operators' ;
import { Transaction , Vout } from '../../interfaces/electrs.interface' ;
2022-08-22 19:12:04 +00:00
import { of , merge , Subscription , Observable , Subject , from } from 'rxjs' ;
2022-08-15 23:14:34 +00:00
import { StateService } from '../../services/state.service' ;
2022-12-27 05:36:58 -06:00
import { CacheService } from '../../services/cache.service' ;
2022-09-21 17:23:45 +02:00
import { OpenGraphService } from '../../services/opengraph.service' ;
import { ApiService } from '../../services/api.service' ;
import { SeoService } from '../../services/seo.service' ;
2023-08-30 20:26:07 +09:00
import { seoDescriptionNetwork } from '../../shared/common.utils' ;
2022-09-21 17:23:45 +02:00
import { CpfpInfo } from '../../interfaces/node-api.interface' ;
2022-08-15 23:14:34 +00:00
import { LiquidUnblinding } from './liquid-ublinding' ;
@Component ( {
selector : 'app-transaction-preview' ,
templateUrl : './transaction-preview.component.html' ,
styleUrls : [ './transaction-preview.component.scss' ] ,
} )
export class TransactionPreviewComponent implements OnInit , OnDestroy {
network = '' ;
tx : Transaction ;
txId : string ;
isLoadingTx = true ;
error : any = undefined ;
errorUnblinded : any = undefined ;
2022-08-24 18:54:11 +00:00
transactionTime = - 1 ;
2022-08-15 23:14:34 +00:00
subscription : Subscription ;
fetchCpfpSubscription : Subscription ;
cpfpInfo : CpfpInfo | null ;
showCpfpDetails = false ;
fetchCpfp $ = new Subject < string > ( ) ;
liquidUnblinding = new LiquidUnblinding ( ) ;
2022-08-22 19:12:04 +00:00
isLiquid = false ;
totalValue : number ;
opReturns : Vout [ ] ;
extraData : 'none' | 'coinbase' | 'opreturn' ;
2022-08-15 23:14:34 +00:00
constructor (
private route : ActivatedRoute ,
private electrsApiService : ElectrsApiService ,
private stateService : StateService ,
2022-12-27 05:36:58 -06:00
private cacheService : CacheService ,
2022-08-15 23:14:34 +00:00
private apiService : ApiService ,
private seoService : SeoService ,
private openGraphService : OpenGraphService ,
) { }
ngOnInit() {
this . stateService . networkChanged $ . subscribe (
2022-08-22 19:12:04 +00:00
( network ) = > {
this . network = network ;
if ( this . network === 'liquid' || this . network == 'liquidtestnet' ) {
this . isLiquid = true ;
}
}
2022-08-15 23:14:34 +00:00
) ;
this . fetchCpfpSubscription = this . fetchCpfp $
. pipe (
switchMap ( ( txId ) = >
2022-12-01 11:34:44 +09:00
this . apiService . getCpfpinfo $ ( txId ) . pipe (
catchError ( ( err ) = > {
return of ( null ) ;
} )
)
2022-08-15 23:14:34 +00:00
)
)
. subscribe ( ( cpfpInfo ) = > {
this . cpfpInfo = cpfpInfo ;
2022-08-31 17:24:56 +00:00
this . openGraphService . waitOver ( 'cpfp-data-' + this . txId ) ;
2022-08-15 23:14:34 +00:00
} ) ;
this . subscription = this . route . paramMap
. pipe (
switchMap ( ( params : ParamMap ) = > {
const urlMatch = ( params . get ( 'id' ) || '' ) . split ( ':' ) ;
this . txId = urlMatch [ 0 ] ;
2022-08-31 17:24:56 +00:00
this . openGraphService . waitFor ( 'tx-data-' + this . txId ) ;
this . openGraphService . waitFor ( 'tx-time-' + this . txId ) ;
2022-08-15 23:14:34 +00:00
this . seoService . setTitle (
$localize ` :@@bisq.transaction.browser-title:Transaction: ${ this . txId } :INTERPOLATION: `
) ;
2023-08-30 20:26:07 +09:00
this . seoService . setDescription ( $localize ` :@@meta.description.bitcoin.transaction:Get real-time status, addresses, fees, script info, and more for ${ this . stateService . network === 'liquid' || this . stateService . network === 'liquidtestnet' ? 'Liquid' : 'Bitcoin' } ${ seoDescriptionNetwork ( this . stateService . network ) } transaction with txid {txid}. ` ) ;
2022-08-15 23:14:34 +00:00
this . resetTransaction ( ) ;
return merge (
of ( true ) ,
this . stateService . connectionState $ . pipe (
filter (
( state ) = > state === 2 && this . tx && ! this . tx . status . confirmed
)
)
) ;
} ) ,
switchMap ( ( ) = > {
let transactionObservable$ : Observable < Transaction > ;
2022-12-27 05:36:58 -06:00
const cached = this . cacheService . getTxFromCache ( this . txId ) ;
2022-11-07 20:05:33 -06:00
if ( cached && cached . fee !== - 1 ) {
transactionObservable $ = of ( cached ) ;
2022-08-15 23:14:34 +00:00
} else {
transactionObservable $ = this . electrsApiService
. getTransaction $ ( this . txId )
. pipe (
catchError ( error = > {
this . error = error ;
this . isLoadingTx = false ;
return of ( null ) ;
} )
) ;
}
return merge (
transactionObservable $ ,
this . stateService . mempoolTransactions $
) ;
} ) ,
switchMap ( ( tx ) = > {
if ( this . network === 'liquid' || this . network === 'liquidtestnet' ) {
return from ( this . liquidUnblinding . checkUnblindedTx ( tx ) )
. pipe (
catchError ( ( error ) = > {
this . errorUnblinded = error ;
return of ( tx ) ;
} )
) ;
}
return of ( tx ) ;
} )
)
. subscribe ( ( tx : Transaction ) = > {
if ( ! tx ) {
2023-03-09 02:34:21 -06:00
this . seoService . logSoft404 ( ) ;
2022-08-31 17:24:56 +00:00
this . openGraphService . fail ( 'tx-data-' + this . txId ) ;
2022-08-15 23:14:34 +00:00
return ;
}
this . tx = tx ;
if ( tx . fee === undefined ) {
this . tx . fee = 0 ;
}
this . tx . feePerVsize = tx . fee / ( tx . weight / 4 ) ;
this . isLoadingTx = false ;
this . error = undefined ;
2022-08-22 19:12:04 +00:00
this . totalValue = this . tx . vout . reduce ( ( acc , v ) = > v . value + acc , 0 ) ;
this . opReturns = this . getOpReturns ( this . tx ) ;
this . extraData = this . chooseExtraData ( ) ;
2022-08-15 23:14:34 +00:00
2022-08-31 18:21:24 +00:00
if ( tx . status . confirmed ) {
this . transactionTime = tx . status . block_time ;
this . openGraphService . waitOver ( 'tx-time-' + this . txId ) ;
} else if ( ! tx . status . confirmed && tx . firstSeen ) {
2022-08-24 18:54:11 +00:00
this . transactionTime = tx . firstSeen ;
2022-08-31 17:24:56 +00:00
this . openGraphService . waitOver ( 'tx-time-' + this . txId ) ;
2022-08-24 18:54:11 +00:00
} else {
this . getTransactionTime ( ) ;
}
2022-11-27 13:47:26 +09:00
if ( this . tx . status . confirmed ) {
this . stateService . markBlock $ . next ( {
blockHeight : tx.status.block_height ,
} ) ;
this . openGraphService . waitFor ( 'cpfp-data-' + this . txId ) ;
this . fetchCpfp $ . next ( this . tx . txid ) ;
} else {
2022-08-15 23:14:34 +00:00
if ( tx . cpfpChecked ) {
2022-11-27 13:47:26 +09:00
this . stateService . markBlock $ . next ( {
txFeePerVSize : tx.effectiveFeePerVsize ,
} ) ;
2022-08-15 23:14:34 +00:00
this . cpfpInfo = {
ancestors : tx.ancestors ,
bestDescendant : tx.bestDescendant ,
} ;
} else {
2022-08-31 17:24:56 +00:00
this . openGraphService . waitFor ( 'cpfp-data-' + this . txId ) ;
2022-08-15 23:14:34 +00:00
this . fetchCpfp $ . next ( this . tx . txid ) ;
}
}
2022-08-31 17:24:56 +00:00
this . openGraphService . waitOver ( 'tx-data-' + this . txId ) ;
2022-08-15 23:14:34 +00:00
} ,
( error ) = > {
2023-03-09 02:34:21 -06:00
this . seoService . logSoft404 ( ) ;
2022-08-31 17:24:56 +00:00
this . openGraphService . fail ( 'tx-data-' + this . txId ) ;
2022-08-15 23:14:34 +00:00
this . error = error ;
this . isLoadingTx = false ;
}
) ;
}
2022-08-24 18:54:11 +00:00
getTransactionTime() {
this . apiService
. getTransactionTimes $ ( [ this . tx . txid ] )
. pipe (
catchError ( ( err ) = > {
return of ( 0 ) ;
} )
)
. subscribe ( ( transactionTimes ) = > {
this . transactionTime = transactionTimes [ 0 ] ;
2022-08-31 17:24:56 +00:00
this . openGraphService . waitOver ( 'tx-time-' + this . txId ) ;
2022-08-24 18:54:11 +00:00
} ) ;
}
2022-08-15 23:14:34 +00:00
resetTransaction() {
this . error = undefined ;
this . tx = null ;
this . isLoadingTx = true ;
2022-08-24 18:54:11 +00:00
this . transactionTime = - 1 ;
2022-08-15 23:14:34 +00:00
this . cpfpInfo = null ;
this . showCpfpDetails = false ;
}
isCoinbase ( tx : Transaction ) : boolean {
return tx . vin . some ( ( v : any ) = > v . is_coinbase === true ) ;
}
haveBlindedOutputValues ( tx : Transaction ) : boolean {
return tx . vout . some ( ( v : any ) = > v . value === undefined ) ;
}
getTotalTxOutput ( tx : Transaction ) {
return tx . vout . map ( ( v : Vout ) = > v . value || 0 ) . reduce ( ( a : number , b : number ) = > a + b ) ;
}
2022-08-22 19:12:04 +00:00
getOpReturns ( tx : Transaction ) : Vout [ ] {
return tx . vout . filter ( ( v ) = > v . scriptpubkey_type === 'op_return' && v . scriptpubkey_asm !== 'OP_RETURN' ) ;
}
chooseExtraData ( ) : 'none' | 'opreturn' | 'coinbase' {
if ( this . isCoinbase ( this . tx ) ) {
return 'coinbase' ;
} else if ( this . opReturns ? . length ) {
return 'opreturn' ;
} else {
return 'none' ;
}
}
2022-08-15 23:14:34 +00:00
ngOnDestroy() {
this . subscription . unsubscribe ( ) ;
this . fetchCpfpSubscription . unsubscribe ( ) ;
}
}