2021-10-19 15:37:45 +04:00
import { Component , OnInit } from '@angular/core' ;
2022-11-28 11:55:23 +09:00
import { UntypedFormBuilder , UntypedFormGroup , Validators } from '@angular/forms' ;
2022-09-21 17:23:45 +02:00
import { ApiService } from '../../services/api.service' ;
2023-08-30 23:59:51 +09:00
import { StateService } from '../../services/state.service' ;
import { SeoService } from '../../services/seo.service' ;
2024-03-10 10:21:11 +09:00
import { OpenGraphService } from '../../services/opengraph.service' ;
2023-08-30 23:59:51 +09:00
import { seoDescriptionNetwork } from '../../shared/common.utils' ;
2024-06-03 21:12:12 +00:00
import { ActivatedRoute , Router } from '@angular/router' ;
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe' ;
2021-10-19 15:37:45 +04:00
@Component ( {
selector : 'app-push-transaction' ,
templateUrl : './push-transaction.component.html' ,
styleUrls : [ './push-transaction.component.scss' ]
} )
export class PushTransactionComponent implements OnInit {
2022-11-28 11:55:23 +09:00
pushTxForm : UntypedFormGroup ;
2021-10-19 15:37:45 +04:00
error : string = '' ;
txId : string = '' ;
isLoading = false ;
constructor (
2022-11-28 11:55:23 +09:00
private formBuilder : UntypedFormBuilder ,
2021-10-19 15:37:45 +04:00
private apiService : ApiService ,
2023-08-30 23:59:51 +09:00
public stateService : StateService ,
private seoService : SeoService ,
2024-03-10 10:21:11 +09:00
private ogService : OpenGraphService ,
2024-06-03 21:12:12 +00:00
private route : ActivatedRoute ,
private router : Router ,
private relativeUrlPipe : RelativeUrlPipe ,
2021-10-19 15:37:45 +04:00
) { }
ngOnInit ( ) : void {
this . pushTxForm = this . formBuilder . group ( {
txHash : [ '' , Validators . required ] ,
} ) ;
2023-08-30 23:59:51 +09:00
this . seoService . setTitle ( $localize ` :@@meta.title.push-tx:Broadcast Transaction ` ) ;
this . seoService . setDescription ( $localize ` :@@meta.description.push-tx:Broadcast a transaction to the ${ this . stateService . network === 'liquid' || this . stateService . network === 'liquidtestnet' ? 'Liquid' : 'Bitcoin' } ${ seoDescriptionNetwork ( this . stateService . network ) } network using the transaction's hash. ` ) ;
2024-03-15 09:35:08 +00:00
this . ogService . setManualOgImage ( 'tx-push.jpg' ) ;
2024-06-03 21:12:12 +00:00
this . route . fragment . subscribe ( async ( fragment ) = > {
const fragmentParams = new URLSearchParams ( fragment || '' ) ;
return this . handleColdcardPushTx ( fragmentParams ) ;
} ) ;
2021-10-19 15:37:45 +04:00
}
2024-06-03 21:12:12 +00:00
async postTx ( hex? : string ) : Promise < string > {
2021-10-19 15:37:45 +04:00
this . isLoading = true ;
this . error = '' ;
this . txId = '' ;
2024-06-03 21:12:12 +00:00
return new Promise ( ( resolve , reject ) = > {
this . apiService . postTransaction $ ( hex || this . pushTxForm . get ( 'txHash' ) . value )
2021-10-19 15:37:45 +04:00
. subscribe ( ( result ) = > {
this . isLoading = false ;
this . txId = result ;
this . pushTxForm . reset ( ) ;
2024-06-03 21:12:12 +00:00
resolve ( this . txId ) ;
2021-10-19 15:37:45 +04:00
} ,
( error ) = > {
if ( typeof error . error === 'string' ) {
const matchText = error . error . match ( '"message":"(.*?)"' ) ;
2024-06-03 21:12:12 +00:00
this . error = 'Failed to broadcast transaction, reason: ' + ( matchText && matchText [ 1 ] || error . error ) ;
2021-10-19 15:37:45 +04:00
} else if ( error . message ) {
2024-06-03 21:12:12 +00:00
this . error = 'Failed to broadcast transaction, reason: ' + error . message ;
2021-10-19 15:37:45 +04:00
}
this . isLoading = false ;
2024-06-03 21:12:12 +00:00
reject ( this . error ) ;
2021-10-19 15:37:45 +04:00
} ) ;
2024-06-03 21:12:12 +00:00
} ) ;
}
private async handleColdcardPushTx ( fragmentParams : URLSearchParams ) : Promise < boolean > {
// maybe conforms to Coldcard nfc-pushtx spec
if ( fragmentParams && fragmentParams . get ( 't' ) ) {
try {
const pushNetwork = fragmentParams . get ( 'n' ) ;
// Redirect to the appropriate network-specific URL
if ( this . stateService . network !== '' && ! pushNetwork ) {
this . router . navigateByUrl ( ` /pushtx# ${ fragmentParams . toString ( ) } ` ) ;
return false ;
} else if ( this . stateService . network !== 'testnet' && pushNetwork === 'XTN' ) {
this . router . navigateByUrl ( ` /testnet/pushtx# ${ fragmentParams . toString ( ) } ` ) ;
return false ;
} else if ( pushNetwork === 'XRT' ) {
this . error = 'Regtest is not supported' ;
return false ;
} else if ( pushNetwork && ! [ 'XTN' , 'XRT' ] . includes ( pushNetwork ) ) {
this . error = 'Invalid network' ;
return false ;
}
const rawTx = this . base64UrlToU8Array ( fragmentParams . get ( 't' ) ) ;
if ( ! fragmentParams . get ( 'c' ) ) {
this . error = 'Missing checksum, URL is probably truncated' ;
return false ;
}
const rawCheck = this . base64UrlToU8Array ( fragmentParams . get ( 'c' ) ) ;
// check checksum
const hashTx = await crypto . subtle . digest ( 'SHA-256' , rawTx ) ;
if ( this . u8ArrayToHex ( new Uint8Array ( hashTx . slice ( 24 ) ) ) !== this . u8ArrayToHex ( rawCheck ) ) {
this . error = 'Bad checksum, URL is probably truncated' ;
return false ;
}
const hexTx = this . u8ArrayToHex ( rawTx ) ;
this . pushTxForm . get ( 'txHash' ) . setValue ( hexTx ) ;
try {
const txid = await this . postTx ( hexTx ) ;
this . router . navigate ( [ this . relativeUrlPipe . transform ( '/tx' ) , txid ] ) ;
} catch ( e ) {
// error already handled
return false ;
}
return true ;
} catch ( e ) {
this . error = 'Failed to decode transaction' ;
return false ;
}
}
}
private base64UrlToU8Array ( base64Url : string ) : Uint8Array {
const base64 = base64Url . replace ( /-/g , '+' ) . replace ( /_/g , '/' ) . padEnd ( base64Url . length + ( 4 - base64Url . length % 4 ) % 4 , '=' ) ;
const binaryString = atob ( base64 ) ;
return new Uint8Array ( [ . . . binaryString ] . map ( char = > char . charCodeAt ( 0 ) ) ) ;
2021-10-19 15:37:45 +04:00
}
2024-06-03 21:12:12 +00:00
private u8ArrayToHex ( arr : Uint8Array ) : string {
return Array . from ( arr ) . map ( byte = > byte . toString ( 16 ) . padStart ( 2 , '0' ) ) . join ( '' ) ;
}
2021-10-19 15:37:45 +04:00
}