import axios from "axios" ; import { BitcoinAPI , DEFAULT_TIMEOUT , sortUTXOs , UTXO } from "./API" ; export enum BlockchairNetwork { BITCOIN = "bitcoin" , BITCOIN_CASH = "bitcoin-cash" , DOGECOIN = "dogecoin" , ZCASH = "zcash" , BITCOIN_TESTNET = "bitcoin/testnet" } export class Blockchair implements BitcoinAPI { public network : BlockchairNetwork ; public constructor ( network = BlockchairNetwork . BITCOIN ) { this . network = network ; } public endpoint ( ) { return `https://api.blockchair.com/${ this . network }` ; } public async fetchHeight ( ) { const url = `${ this . endpoint ( ) }/stats` ; const response = ( await axios . get < { data : { best_block_height : number ; } ; } > ( `${ url }` , { timeout : DEFAULT_TIMEOUT , } ) ) . data ; return response . data . best_block_height . toString ( ) ; } public async fetchUTXO ( txid , txindex ) { const url = `${ this . endpoint ( ) }/dashboards/transaction/${ txid }` ; const response = ( await axios . get < TransactionResponse > ( `${ url }` , { timeout : DEFAULT_TIMEOUT , } ) ) . data ; if ( ! response . data [ txid ] ) { throw new Error ( `Transaction not found.` ) ; } const tx = response . data [ txid ] ; let latestBlock = response . context . state ; if ( latestBlock === 0 ) { const statsUrl = `${ this . endpoint ( ) }/stats` ; const statsResponse = ( await axios . get ( statsUrl , { timeout : DEFAULT_TIMEOUT } ) ) . data ; latestBlock = statsResponse . data . blocks - 1 ; } return { txid : txid , txindex : txindex , amount : tx . outputs [ parseInt ( txindex , 10 ) ] . value . toString ( ) , height : tx . transaction . block_id && tx . transaction . block_id > 0 ? tx . transaction . block_id . toString ( ) : null , } ; } public async fetchUTXOs ( address ) { const url = `${ this . endpoint ( ) }/dashboards/address/${ address }?limit=0,100` ; const response = ( await axios . get < AddressResponse > ( url , { timeout : DEFAULT_TIMEOUT } ) ) . data ; let latestBlock = response . context . state ; if ( latestBlock === 0 ) { const statsUrl = `${ this . endpoint ( ) }/stats` ; const statsResponse = ( await axios . get ( statsUrl , { timeout : DEFAULT_TIMEOUT } ) ) . data ; latestBlock = statsResponse . data . blocks - 1 ; } return response . data [ address ] . utxo . map ( ( utxo ) => ( { txid : utxo . transaction_hash , amount : utxo . value . toString ( ) , txindex : utxo . index . toString ( ) , height : utxo . block_id && utxo . block_id > 0 ? utxo . block_id . toString ( ) : null , } ) ) . sort ( sortUTXOs ) ; } public async fetchTXs ( address , limit = 25 ) { const url = `${ this . endpoint ( ) }/dashboards/address/${ address }?limit=${ limit },0` ; const response = ( await axios . get < AddressResponse > ( url , { timeout : DEFAULT_TIMEOUT } ) ) . data ; const txHashes = response . data [ address ] . transactions ; let txDetails = { } ; for ( let i = 0 ; i < Math . ceil ( txHashes . length / 10 ) ; i ++ ) { const txUrl = `${ this . endpoint ( ) }/dashboards/transactions/${ txHashes . slice ( i * 10 , ( i + 1 ) * 10 ) . join ( "," ) }` ; const txResponse = ( await axios . get < TransactionResponse > ( txUrl , { timeout : DEFAULT_TIMEOUT , } ) ) . data ; txDetails = { ... txDetails , ... txResponse . data , } ; } const received = [ ] ; for ( const txHash of txHashes ) { const tx = txDetails [ txHash ] ; for ( let i = 0 ; i < tx . outputs . length ; i ++ ) { const output = tx . outputs [ i ] ; if ( output . recipient === address ) { received . push ( { txid : tx . transaction . hash , amount : output . value . toString ( ) , txindex : i . toString ( ) , height : output . block_id && output . block_id > 0 ? output . block_id . toString ( ) : null , } ) ; } } } return received . sort ( sortUTXOs ) ; } public async broadcastTransaction ( txHex ) { const url = `${ this . endpoint ( ) }/push/transaction` ; const response = await axios . post < { data : { transaction_hash : string ; } ; } > ( url , { data : txHex } , { timeout : DEFAULT_TIMEOUT } ) ; if ( ( response . data as unknown as BlockchairError ) . error ) { throw new Error ( ( response . data as unknown as BlockchairError ) . error ) ; } return response . data . data . transaction_hash ; } } interface BlockchairError { error : string ; } interface BlockchairContext { code : number ; source : string ; time : number ; limit : string ; offset : string ; results : number ; state : number ; cache : { live : boolean ; duration : number ; since : string ; until : string ; time : null ; } ; api : { version : string ; last_major_update : string ; next_major_update : null | string ; documentation : "https://blockchair.com/api/docs" ; notice ? : string ; } ; } /** TYPES */ interface AddressResponse { data : { [ addr ] : { address : { type : "pubkey" ; script_hex : string ; balance : number ; balance_usd : number ; received : number ; received_usd : number ; spent : number ; spent_usd : number ; output_count : number ; unspent_output_count : number ; first_seen_receiving : string ; last_seen_receiving : string ; first_seen_spending : null ; last_seen_spending : null ; transaction_count : null ; } ; transactions : string [ ] ; utxo : Array < { block_id : number ; transaction_hash : string ; index : number ; value : number ; } > ; } ; } ; context : BlockchairContext ; } interface InputOrOutput { block_id : number ; transaction_id : number ; index : number ; transaction_hash : string ; date : string ; time : string ; value : number ; value_usd : number ; recipient : string ; type : string ; script_hex : string ; is_from_coinbase : boolean ; is_spendable : boolean ; is_spent : boolean ; spending_block_id : number ; spending_transaction_id : number ; spending_index : number ; spending_transaction_hash : string ; spending_date : string ; spending_time : string ; spending_value_usd : number ; spending_sequence : number ; spending_signature_hex : string ; spending_witness : string ; lifespan : number ; cdd : number ; } interface TransactionResponse { data : { [ utxo ] : { transaction : { block_id : number ; id : number ; hash : string ; date : string ; time : string ; size : number ; weight : number ; version : number ; lock_time : number ; is_coinbase : boolean ; has_witness : boolean ; input_count : number ; output_count : number ; input_total : number ; input_total_usd : number ; output_total : number ; output_total_usd : number ; fee : number ; fee_usd : number ; fee_per_kb : number ; fee_per_kb_usd : number ; fee_per_kwu : number ; fee_per_kwu_usd : number ; cdd_total : number ; is_rbf : boolean ; } ; inputs : InputOrOutput [ ] ; outputs : InputOrOutput [ ] ; } ; } ; context : BlockchairContext ; }