2020-02-23 19:16:50 +07:00
import { Component , OnInit , OnDestroy } from '@angular/core' ;
2019-11-13 14:51:44 +08:00
import { ActivatedRoute , ParamMap } from '@angular/router' ;
2020-02-16 22:15:07 +07:00
import { ElectrsApiService } from '../../services/electrs-api.service' ;
2021-01-08 21:44:36 +07:00
import { switchMap , filter , catchError , map , tap } from 'rxjs/operators' ;
2023-07-22 17:51:45 +09:00
import { Address , ScriptHash , Transaction } from '../../interfaces/electrs.interface' ;
2022-09-21 17:23:45 +02:00
import { WebsocketService } from '../../services/websocket.service' ;
import { StateService } from '../../services/state.service' ;
import { AudioService } from '../../services/audio.service' ;
import { ApiService } from '../../services/api.service' ;
2021-01-08 21:44:36 +07:00
import { of , merge , Subscription , Observable } from 'rxjs' ;
2022-09-21 17:23:45 +02:00
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 { AddressInformation } from '../../interfaces/node-api.interface' ;
2019-11-10 16:44:00 +08:00
@Component ( {
selector : 'app-address' ,
templateUrl : './address.component.html' ,
styleUrls : [ './address.component.scss' ]
} )
2020-02-23 19:16:50 +07:00
export class AddressComponent implements OnInit , OnDestroy {
2020-05-09 20:37:50 +07:00
network = '' ;
2020-03-25 21:55:18 +07:00
2020-02-16 22:15:07 +07:00
address : Address ;
addressString : string ;
2019-11-13 14:51:44 +08:00
isLoadingAddress = true ;
2020-02-16 22:15:07 +07:00
transactions : Transaction [ ] ;
2019-11-13 14:51:44 +08:00
isLoadingTransactions = true ;
2021-10-12 10:54:14 -03:00
retryLoadMore = false ;
2019-11-13 14:51:44 +08:00
error : any ;
2020-04-12 03:03:51 +07:00
mainSubscription : Subscription ;
2024-04-09 14:11:57 +09:00
mempoolTxSubscription : Subscription ;
mempoolRemovedTxSubscription : Subscription ;
blockTxSubscription : Subscription ;
2021-01-08 21:44:36 +07:00
addressLoadingStatus$ : Observable < number > ;
2021-09-06 10:20:31 +04:00
addressInfo : null | AddressInformation = null ;
2020-02-26 04:29:57 +07:00
2024-02-06 21:39:29 +00:00
fullyLoaded = false ;
2020-02-26 04:29:57 +07:00
txCount = 0 ;
2021-10-12 10:54:14 -03:00
received = 0 ;
2020-02-26 04:29:57 +07:00
sent = 0 ;
2024-04-29 20:54:56 +00:00
now = Date . now ( ) / 1000 ;
balancePeriod : 'all' | '1m' = 'all' ;
2019-11-10 16:44:00 +08:00
2020-02-28 03:51:59 +07:00
private tempTransactions : Transaction [ ] ;
private timeTxIndexes : number [ ] ;
private lastTransactionTxId : string ;
2019-11-13 14:51:44 +08:00
constructor (
private route : ActivatedRoute ,
2020-02-16 22:15:07 +07:00
private electrsApiService : ElectrsApiService ,
2020-02-23 19:16:50 +07:00
private websocketService : WebsocketService ,
2024-04-01 03:49:56 +00:00
public stateService : StateService ,
2020-02-26 04:29:57 +07:00
private audioService : AudioService ,
2020-02-28 01:09:07 +07:00
private apiService : ApiService ,
2020-03-24 00:52:08 +07:00
private seoService : SeoService ,
2019-11-13 14:51:44 +08:00
) { }
2019-11-10 16:44:00 +08:00
ngOnInit() {
2020-05-09 20:37:50 +07:00
this . stateService . networkChanged $ . subscribe ( ( network ) = > this . network = network ) ;
2021-01-08 21:44:36 +07:00
this . websocketService . want ( [ 'blocks' ] ) ;
this . addressLoadingStatus $ = this . route . paramMap
. pipe (
switchMap ( ( ) = > this . stateService . loadingIndicators $ ) ,
map ( ( indicators ) = > indicators [ 'address-' + this . addressString ] !== undefined ? indicators [ 'address-' + this . addressString ] : 0 )
) ;
2020-02-23 19:16:50 +07:00
2020-04-12 03:03:51 +07:00
this . mainSubscription = this . route . paramMap
2020-03-24 21:38:11 +07:00
. pipe (
switchMap ( ( params : ParamMap ) = > {
this . error = undefined ;
this . isLoadingAddress = true ;
2024-02-06 21:39:29 +00:00
this . fullyLoaded = false ;
2020-03-24 21:38:11 +07:00
this . address = null ;
this . isLoadingTransactions = true ;
this . transactions = null ;
2021-09-06 10:20:31 +04:00
this . addressInfo = null ;
2020-03-24 21:38:11 +07:00
document . body . scrollTo ( 0 , 0 ) ;
this . addressString = params . get ( 'id' ) || '' ;
2023-07-28 16:35:42 +09:00
if ( /^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/ . test ( this . addressString ) ) {
2021-09-05 00:30:24 +04:00
this . addressString = this . addressString . toLowerCase ( ) ;
}
2020-12-04 21:29:31 +07:00
this . seoService . setTitle ( $localize ` :@@address.component.browser-title:Address: ${ this . addressString } :INTERPOLATION: ` ) ;
2023-08-30 20:26:07 +09:00
this . seoService . setDescription ( $localize ` :@@meta.description.bitcoin.address:See mempool transactions, confirmed transactions, balance, and more for ${ this . stateService . network === 'liquid' || this . stateService . network === 'liquidtestnet' ? 'Liquid' : 'Bitcoin' } ${ seoDescriptionNetwork ( this . stateService . network ) } address ${ this . addressString } :INTERPOLATION:. ` ) ;
2020-03-24 21:38:11 +07:00
return merge (
of ( true ) ,
this . stateService . connectionState $
. pipe ( filter ( ( state ) = > state === 2 && this . transactions && this . transactions . length > 0 ) )
)
. pipe (
2023-07-22 17:51:45 +09:00
switchMap ( ( ) = > (
2023-07-28 16:35:42 +09:00
this . addressString . match ( /04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/ )
2023-07-22 17:51:45 +09:00
? this . electrsApiService . getPubKeyAddress $ ( this . addressString )
: this . electrsApiService . getAddress $ ( this . addressString )
) . pipe (
2020-03-30 14:54:48 +07:00
catchError ( ( err ) = > {
this . isLoadingAddress = false ;
this . error = err ;
2023-03-09 02:34:21 -06:00
this . seoService . logSoft404 ( ) ;
2020-03-30 14:54:48 +07:00
console . log ( err ) ;
return of ( null ) ;
} )
)
)
2020-03-24 21:38:11 +07:00
) ;
} )
)
2020-02-28 01:09:07 +07:00
. pipe (
2020-03-30 14:54:48 +07:00
filter ( ( address ) = > ! ! address ) ,
2021-09-06 10:20:31 +04:00
tap ( ( address : Address ) = > {
2024-02-05 11:22:14 +01:00
if ( ( this . stateService . network === 'liquid' || this . stateService . network === 'liquidtestnet' ) && /^([a-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/ . test ( address . address ) ) {
2021-09-06 10:20:31 +04:00
this . apiService . validateAddress $ ( address . address )
. subscribe ( ( addressInfo ) = > {
this . addressInfo = addressInfo ;
this . websocketService . startTrackAddress ( addressInfo . unconfidential ) ;
} ) ;
} else {
this . websocketService . startTrackAddress ( address . address ) ;
}
} ) ,
2020-02-28 01:09:07 +07:00
switchMap ( ( address ) = > {
this . address = address ;
this . updateChainStats ( ) ;
this . isLoadingAddress = false ;
this . isLoadingTransactions = true ;
2023-07-22 17:51:45 +09:00
return address . is_pubkey
2023-07-28 16:35:42 +09:00
? this . electrsApiService . getScriptHashTransactions $ ( ( address . address . length === 66 ? '21' : '41' ) + address . address + 'ac' )
2023-07-22 17:51:45 +09:00
: this . electrsApiService . getAddressTransactions $ ( address . address ) ;
2020-02-28 01:09:07 +07:00
} ) ,
switchMap ( ( transactions ) = > {
this . tempTransactions = transactions ;
2020-03-30 14:54:48 +07:00
if ( transactions . length ) {
this . lastTransactionTxId = transactions [ transactions . length - 1 ] . txid ;
}
2020-02-28 03:51:59 +07:00
const fetchTxs : string [ ] = [ ] ;
this . timeTxIndexes = [ ] ;
transactions . forEach ( ( tx , index ) = > {
if ( ! tx . status . confirmed ) {
fetchTxs . push ( tx . txid ) ;
this . timeTxIndexes . push ( index ) ;
}
} ) ;
if ( ! fetchTxs . length ) {
return of ( [ ] ) ;
}
2024-03-21 07:57:13 +00:00
return this . apiService . getTransactionTimes $ ( fetchTxs ) . pipe (
catchError ( ( err ) = > {
this . isLoadingAddress = false ;
this . isLoadingTransactions = false ;
this . error = err ;
this . seoService . logSoft404 ( ) ;
console . log ( err ) ;
return of ( [ ] ) ;
} )
) ;
2020-02-28 01:09:07 +07:00
} )
)
2024-03-21 07:57:13 +00:00
. subscribe ( ( times : number [ ] | null ) = > {
if ( ! times ) {
return ;
}
2020-02-28 01:09:07 +07:00
times . forEach ( ( time , index ) = > {
2020-02-28 03:51:59 +07:00
this . tempTransactions [ this . timeTxIndexes [ index ] ] . firstSeen = time ;
2020-02-28 01:09:07 +07:00
} ) ;
this . tempTransactions . sort ( ( a , b ) = > {
2021-09-25 16:28:11 +04:00
if ( b . status . confirmed ) {
if ( b . status . block_height === a . status . block_height ) {
return b . status . block_time - a . status . block_time ;
}
return b . status . block_height - a . status . block_height ;
}
return b . firstSeen - a . firstSeen ;
2020-02-28 01:09:07 +07:00
} ) ;
this . transactions = this . tempTransactions ;
2024-05-13 17:16:28 +07:00
if ( this . transactions . length === this . txCount ) {
this . fullyLoaded = true ;
}
2020-02-28 01:09:07 +07:00
this . isLoadingTransactions = false ;
2024-04-29 20:54:56 +00:00
if ( ! this . showBalancePeriod ( ) ) {
this . setBalancePeriod ( 'all' ) ;
}
2020-02-26 23:21:16 +07:00
} ,
( error ) = > {
console . log ( error ) ;
this . error = error ;
2023-03-09 02:34:21 -06:00
this . seoService . logSoft404 ( ) ;
2020-02-26 23:21:16 +07:00
this . isLoadingAddress = false ;
} ) ;
2020-03-24 21:38:11 +07:00
2024-04-09 14:11:57 +09:00
this . mempoolTxSubscription = this . stateService . mempoolTransactions $
2023-07-28 15:53:52 +09:00
. subscribe ( tx = > {
this . addTransaction ( tx ) ;
2020-03-24 21:38:11 +07:00
} ) ;
2024-04-09 14:11:57 +09:00
this . mempoolRemovedTxSubscription = this . stateService . mempoolRemovedTransactions $
2023-08-25 00:57:47 +09:00
. subscribe ( tx = > {
this . removeTransaction ( tx ) ;
} ) ;
2024-04-09 14:11:57 +09:00
this . blockTxSubscription = this . stateService . blockTransactions $
2020-03-24 21:38:11 +07:00
. subscribe ( ( transaction ) = > {
const tx = this . transactions . find ( ( t ) = > t . txid === transaction . txid ) ;
if ( tx ) {
tx . status = transaction . status ;
this . transactions = this . transactions . slice ( ) ;
this . audioService . playSound ( 'magic' ) ;
2023-07-28 15:53:52 +09:00
} else {
if ( this . addTransaction ( transaction , false ) ) {
this . audioService . playSound ( 'magic' ) ;
}
2020-03-24 21:38:11 +07:00
}
} ) ;
2020-02-26 23:21:16 +07:00
}
2023-07-28 15:53:52 +09:00
addTransaction ( transaction : Transaction , playSound : boolean = true ) : boolean {
if ( this . transactions . some ( ( t ) = > t . txid === transaction . txid ) ) {
return false ;
}
this . transactions . unshift ( transaction ) ;
this . transactions = this . transactions . slice ( ) ;
this . txCount ++ ;
if ( playSound ) {
if ( transaction . vout . some ( ( vout ) = > vout ? . scriptpubkey_address === this . address . address ) ) {
this . audioService . playSound ( 'cha-ching' ) ;
} else {
this . audioService . playSound ( 'chime' ) ;
}
}
transaction . vin . forEach ( ( vin ) = > {
if ( vin ? . prevout ? . scriptpubkey_address === this . address . address ) {
this . sent += vin . prevout . value ;
}
} ) ;
transaction . vout . forEach ( ( vout ) = > {
if ( vout ? . scriptpubkey_address === this . address . address ) {
this . received += vout . value ;
}
} ) ;
return true ;
}
2023-08-25 00:57:47 +09:00
removeTransaction ( transaction : Transaction ) : boolean {
const index = this . transactions . findIndex ( ( ( tx ) = > tx . txid === transaction . txid ) ) ;
if ( index === - 1 ) {
return false ;
}
this . transactions . splice ( index , 1 ) ;
this . transactions = this . transactions . slice ( ) ;
this . txCount -- ;
transaction . vin . forEach ( ( vin ) = > {
if ( vin ? . prevout ? . scriptpubkey_address === this . address . address ) {
this . sent -= vin . prevout . value ;
}
} ) ;
transaction . vout . forEach ( ( vout ) = > {
if ( vout ? . scriptpubkey_address === this . address . address ) {
this . received -= vout . value ;
}
} ) ;
return true ;
}
2019-11-13 14:51:44 +08:00
loadMore() {
2024-02-06 21:39:29 +00:00
if ( this . isLoadingTransactions || this . fullyLoaded ) {
2020-03-01 03:32:12 +07:00
return ;
}
2019-11-13 14:51:44 +08:00
this . isLoadingTransactions = true ;
2021-10-12 10:54:14 -03:00
this . retryLoadMore = false ;
2024-03-17 17:58:26 +09:00
( this . address . is_pubkey
? this . electrsApiService . getScriptHashTransactions $ ( ( this . address . address . length === 66 ? '21' : '41' ) + this . address . address + 'ac' , this . lastTransactionTxId )
: this . electrsApiService . getAddressTransactions $ ( this . address . address , this . lastTransactionTxId ) )
2020-02-28 04:16:15 +07:00
. subscribe ( ( transactions : Transaction [ ] ) = > {
2024-02-06 21:39:29 +00:00
if ( transactions && transactions . length ) {
this . lastTransactionTxId = transactions [ transactions . length - 1 ] . txid ;
this . transactions = this . transactions . concat ( transactions ) ;
2024-02-07 20:23:35 +00:00
} else {
2024-02-06 21:39:29 +00:00
this . fullyLoaded = true ;
}
2019-11-13 14:51:44 +08:00
this . isLoadingTransactions = false ;
2021-06-06 16:07:45 -04:00
} ,
( error ) = > {
this . isLoadingTransactions = false ;
2021-10-12 10:54:14 -03:00
this . retryLoadMore = true ;
2023-07-13 16:57:36 +09:00
// In the unlikely event of the txid wasn't found in the mempool anymore and we must reload the page.
if ( error . status === 422 ) {
window . location . reload ( ) ;
}
2019-11-13 14:51:44 +08:00
} ) ;
}
2020-02-23 19:16:50 +07:00
2020-02-28 04:16:15 +07:00
updateChainStats() {
2021-10-12 10:54:14 -03:00
this . received = this . address . chain_stats . funded_txo_sum + this . address . mempool_stats . funded_txo_sum ;
2020-02-28 04:16:15 +07:00
this . sent = this . address . chain_stats . spent_txo_sum + this . address . mempool_stats . spent_txo_sum ;
this . txCount = this . address . chain_stats . tx_count + this . address . mempool_stats . tx_count ;
}
2024-04-29 20:54:56 +00:00
setBalancePeriod ( period : 'all' | '1m' ) : boolean {
this . balancePeriod = period ;
return false ;
}
showBalancePeriod ( ) : boolean {
return this . transactions ? . length && (
! this . transactions [ 0 ] . status ? . confirmed
|| this . transactions [ 0 ] . status . block_time > ( this . now - ( 60 * 60 * 24 * 30 ) )
) ;
}
2020-02-23 19:16:50 +07:00
ngOnDestroy() {
2020-04-12 03:03:51 +07:00
this . mainSubscription . unsubscribe ( ) ;
2024-04-09 14:11:57 +09:00
this . mempoolTxSubscription . unsubscribe ( ) ;
this . mempoolRemovedTxSubscription . unsubscribe ( ) ;
this . blockTxSubscription . unsubscribe ( ) ;
2020-04-12 03:03:51 +07:00
this . websocketService . stopTrackingAddress ( ) ;
2020-02-23 19:16:50 +07:00
}
2019-11-10 16:44:00 +08:00
}