import fetch , { RequestInit } from 'node-fetch' ; import { parsePort , stopwatch , logError , timeout } from '../helpers' ; import { CoreNodeFeeResponse } from '@stacks/stacks-blockchain-api-types' ; interface CoreRpcAccountInfo { /** Hex-prefixed uint128. */ balance : string ; /** Hex-prefixed binary blob. */ balance_proof : string ; locked : string ; nonce : number ; /** Hex-prefixed binary blob. */ nonce_proof : string ; } interface CoreRpcInfo { burn_block_height : number ; burn_consensus : string ; exit_at_block_height : number | null ; network_id : number ; parent_network_id : number ; peer_version : number ; server_version : string ; stable_burn_block_height : number ; stable_burn_consensus : string ; stacks_tip : string ; stacks_tip_burn_block : string ; stacks_tip_height : number ; unanchored_tip : string ; } interface CoreRpcPoxInfo { contract_id : string ; first_burnchain_block_height : number ; min_amount_ustx : number ; registration_window_length : number ; rejection_fraction : number ; reward_cycle_id : number ; reward_cycle_length : number ; } export interface Neighbor { network_id : number ; peer_version : number ; ip : string ; port : number ; public_key_hash : string ; authenticated : boolean ; } interface CoreRpcNeighbors { sample : Neighbor [ ] ; inbound : Neighbor [ ] ; outbound : Neighbor [ ] ; } type RequestOpts = RequestInit & { queryParams ? : Record < string , string > ; } ; export function getCoreNodeEndpoint ( opts ? ) { const host = opts ?. host ?? process . env [ 'STACKS_CORE_RPC_HOST' ] ; if ( ! host ) { throw new Error ( `STACKS_CORE_RPC_HOST is not defined` ) ; } const port = parsePort ( opts ?. port ?? process . env [ 'STACKS_CORE_RPC_PORT' ] ) ; if ( ! port ) { throw new Error ( `STACKS_CORE_RPC_PORT is not defined` ) ; } return `${ host }:${ port }` ; } export class StacksCoreRpcClient { readonly endpoint : string ; constructor ( opts ? ) { this . endpoint = getCoreNodeEndpoint ( opts ) ; } createUrl ( path , init ? ) { const url = new URL ( `http://${ this . endpoint }/${ path }` ) ; if ( init ?. queryParams ) { Object . entries ( init . queryParams ) . forEach ( ( [ k , v ] ) => url . searchParams . set ( k , v ) ) ; } return url . toString ( ) ; } param retryTimeout async waitForConnection ( retryTimeout = 30000 ) { const retryInterval = 1000 ; const timer = stopwatch ( ) ; let lastError ; do { try { const info = await this . getInfo ( ) ; if ( ! info . stacks_tip_height || info . stacks_tip_height <= 0 ) { throw new Error ( `stacks_tip_height not >= 1` ) ; } return ; } catch ( error ) { lastError = error ; await timeout ( retryInterval ) ; } } while ( timer . getElapsed ( ) < retryTimeout ) ; throw lastError ; } async fetchJson < T > ( path , init ? ) { const resultString = await this . fetchText ( path , init ) ; try { const resultJson = JSON . parse ( resultString ) ; return resultJson ; } catch ( error ) { logError ( `Error parsing json: "${ resultString }"` , error ) ; throw error ; } } async fetchText ( path , init ? ) { const url = this . createUrl ( path , init ) ; const result = await fetch ( url , init ) ; if ( ! result . ok ) { let msg = '' ; try { msg = await result . text ( ) ; } catch ( error ) { } throw new Error ( `Response ${ result . status }: ${ result . statusText } fetching ${ url } - ${ msg }` ) ; } try { const resultString = await result . text ( ) ; return resultString ; } catch ( error ) { logError ( `Error reading response from ${ url }` , error ) ; throw error ; } } async getInfo ( ) { const result = await this . fetchJson < CoreRpcInfo > ( 'v2/info' ) ; return result ; } async getPox ( ) { const result = await this . fetchJson < CoreRpcPoxInfo > ( 'v2/pox' ) ; return result ; } async getAccount ( principal , atUnanchoredChainTip = false , indexBlockHash ? ) { const requestOpts = { method : 'GET' , queryParams : { proof : '0' , } , } ; if ( atUnanchoredChainTip ) { const info = await this . getInfo ( ) ; requestOpts . queryParams ! . tip = info . unanchored_tip ; } else if ( indexBlockHash ) { requestOpts . queryParams ! . tip = indexBlockHash ; } const result = await this . fetchJson < CoreRpcAccountInfo > ( `v2/accounts/${ principal }` , requestOpts ) ; return result ; } async getAccountNonce ( principal , atUnanchoredChainTip = false ) { const nonces = [ ] ; const lookups = [ this . getAccount ( principal , false ) . then ( account => nonces . push ( account . nonce ) ) , ] ; if ( atUnanchoredChainTip ) { lookups . push ( this . getAccount ( principal , true ) . then ( account => nonces . push ( account . nonce ) ) ) ; } await Promise . allSettled ( lookups ) ; if ( nonces . length === 0 ) { await lookups [ 0 ] ; } const nonce = Math . max ( ... nonces ) ; return nonce ; } async getAccountBalance ( principal ) { const account = await this . getAccount ( principal ) ; const balance = BigInt ( account . balance ) ; return balance ; } async sendTransaction ( serializedTx ) { const result = await this . fetchJson < string > ( 'v2/transactions' , { method : 'POST' , headers : { 'Content-Type' : 'application/octet-stream' } , body : serializedTx , } ) ; return { txId : '0x' + result , } ; } async getNeighbors ( ) { const result = await this . fetchJson < CoreRpcNeighbors > ( `v2/neighbors` , { method : 'GET' , } ) ; return result ; } async getEstimatedTransferFee ( ) { const result = await this . fetchJson < CoreNodeFeeResponse > ( `v2/fees/transfer` , { method : 'GET' , } ) ; return result ; } }