@@ -10,6 +10,8 @@ import { getEsploraApiBaseUrl, getEsploraExplorerBaseUrl } from '../config/runti
1010
1111/** Default request timeout in milliseconds. */
1212export const DEFAULT_TIMEOUT_MS = 30_000
13+ export const DEFAULT_ESPLORA_FEE_TARGET_BLOCKS = 1
14+ export const DEFAULT_FEE_RATE_SAT_KVB = 100
1315
1416function normalizeBaseUrl ( value ?: string ) : string | undefined {
1517 if ( typeof value !== 'string' || ! value . trim ( ) ) {
@@ -30,6 +32,41 @@ function getExplorerBaseUrl(apiBaseUrl: string): string {
3032 return apiBaseUrl
3133}
3234
35+ function isValidFeeEstimateEntry ( target : number , rate : number ) : boolean {
36+ return Number . isInteger ( target ) && target > 0 && Number . isFinite ( rate ) && rate > 0
37+ }
38+
39+ function parseFeeEstimateEntries (
40+ estimates : Record < string , number >
41+ ) : Array < readonly [ number , number ] > {
42+ return Object . entries ( estimates )
43+ . map ( ( [ target , rate ] ) => [ Number ( target ) , rate ] as const )
44+ . filter ( ( [ target , rate ] ) => isValidFeeEstimateEntry ( target , rate ) )
45+ . sort ( ( [ leftTarget ] , [ rightTarget ] ) => leftTarget - rightTarget )
46+ }
47+
48+ export function selectFeeRateSatVb (
49+ estimates : Record < string , number > ,
50+ targetBlocks : number
51+ ) : number {
52+ const entries = parseFeeEstimateEntries ( estimates )
53+ if ( entries . length === 0 ) {
54+ throw new EsploraApiError ( 'No fee estimates available' )
55+ }
56+
57+ const exactEntry = entries . find ( ( [ target ] ) => target === targetBlocks )
58+ if ( exactEntry ) {
59+ return exactEntry [ 1 ]
60+ }
61+
62+ const higherTargetEntry = entries . find ( ( [ target ] ) => target > targetBlocks )
63+ if ( higherTargetEntry ) {
64+ return higherTargetEntry [ 1 ]
65+ }
66+
67+ return entries [ 0 ] [ 1 ]
68+ }
69+
3370export class EsploraApiError extends Error {
3471 readonly status : number | undefined
3572 readonly body : string | undefined
@@ -162,6 +199,39 @@ export class EsploraClient {
162199 return height
163200 }
164201
202+ /** GET /fee-estimates — confirmation target to fee rate map in sat/vB. */
203+ async getFeeEstimates ( ) : Promise < Record < string , number > > {
204+ const body = await this . get ( '/fee-estimates' )
205+ try {
206+ const raw = JSON . parse ( body ) as unknown
207+ if ( raw === null || typeof raw !== 'object' || Array . isArray ( raw ) ) {
208+ throw new EsploraApiError ( 'Expected fee estimate object' )
209+ }
210+
211+ return Object . fromEntries (
212+ Object . entries ( raw ) . filter (
213+ ( [ target , rate ] ) =>
214+ Number . isInteger ( Number ( target ) ) &&
215+ Number ( target ) > 0 &&
216+ typeof rate === 'number' &&
217+ Number . isFinite ( rate ) &&
218+ rate > 0
219+ )
220+ )
221+ } catch ( e ) {
222+ if ( e instanceof EsploraApiError ) throw e
223+ throw new EsploraApiError (
224+ `Failed to parse fee estimates: ${ e instanceof Error ? e . message : String ( e ) } `
225+ )
226+ }
227+ }
228+
229+ /** Resolve a fee rate for the target in sats/kvB. */
230+ async getFeeRateSatKvb ( targetBlocks : number ) : Promise < number > {
231+ const feeRateSatVb = selectFeeRateSatVb ( await this . getFeeEstimates ( ) , targetBlocks )
232+ return feeRateSatVb * 1000
233+ }
234+
165235 /** Block hash at a given height. */
166236 async getBlockHashAtHeight ( blockHeight : number ) : Promise < string > {
167237 const body = await this . get ( `/block-height/${ blockHeight } ` )
@@ -383,6 +453,18 @@ export interface EsploraOutspend {
383453 [ key : string ] : unknown
384454}
385455
456+ export async function resolveWalletFeeRateSatKvb (
457+ esplora : Pick < EsploraClient , 'getFeeRateSatKvb' > ,
458+ targetBlocks : number = DEFAULT_ESPLORA_FEE_TARGET_BLOCKS ,
459+ fallbackFeeRateSatKvb : number = DEFAULT_FEE_RATE_SAT_KVB
460+ ) : Promise < number > {
461+ try {
462+ return await esplora . getFeeRateSatKvb ( targetBlocks )
463+ } catch {
464+ return fallbackFeeRateSatKvb
465+ }
466+ }
467+
386468/**
387469 * Hash script_pubkey (hex) to 32-byte script hash (SHA256).
388470 * Matches Rust hash_script; use this for PreLockArguments script hashes.
0 commit comments