diff --git a/src/server.tsx b/src/server.tsx index fdfd147..1802fa1 100644 --- a/src/server.tsx +++ b/src/server.tsx @@ -32,37 +32,18 @@ console.info(`Using cache stdTTL: ${stdTTL}`); console.info(`Using cache checkperiod: ${checkperiod}`); console.info('---'); +// Constants +const MEMPOOL_TIP_HASH_URL = `${mempoolBaseUrl}/api/blocks/tip/hash`; +const ESPLORA_TIP_HASH_URL = `${esploraBaseUrl}/api/blocks/tip/hash`; +const MEMPOOL_FEES_URL = `${mempoolBaseUrl}/api/v1/fees/recommended`; +const ESPLORA_FEE_ESTIMATES_URL = `${esploraBaseUrl}/api/fee-estimates`; + // Initialize the cache. const cache = new NodeCache({ stdTTL: stdTTL, checkperiod: checkperiod }); const CACHE_KEY = 'estimates'; -/** - * Fetches data from the given URL with a timeout. - */ -// async function fetchWithTimeout(url: string, timeout: number = TIMEOUT): Promise { -// const controller = new AbortController(); -// const id = setTimeout(() => controller.abort(), timeout); - -// console.debug(`Starting fetch request to ${url}`); - -// try { -// const response = await fetch(url, { signal: controller.signal }); -// clearTimeout(id); - -// console.debug(`Successfully fetched data from ${url}`); -// return response; -// } catch (error: any) { -// if (error.name === 'AbortError') { -// console.error(`Fetch request to ${url} timed out after ${timeout} ms`); -// throw new Error(`Request timed out after ${timeout} ms`); -// } else { -// console.error(`Error fetching data from ${url}:`, error); -// throw error; -// } -// } -// } - -// FIXME: fetch signal abortcontroller does not work on Bun. See https://github.com/oven-sh/bun/issues/2489 +// NOTE: fetch signal abortcontroller does not work on Bun. +// See https://github.com/oven-sh/bun/issues/2489 async function fetchWithTimeout(url: string, timeout: number = TIMEOUT): Promise { console.debug(`Starting fetch request to ${url}`); const fetchPromise = fetch(url); @@ -70,7 +51,7 @@ async function fetchWithTimeout(url: string, timeout: number = TIMEOUT): Promise setTimeout(() => reject(new Error(`Request timed out after ${timeout} ms`)), timeout) ); - return Promise.race([fetchPromise, timeoutPromise]); + return Promise.race([fetchPromise, timeoutPromise]) as Promise; } /** @@ -122,10 +103,10 @@ app.use('/static/*', serveStatic({ root: './' })) */ async function fetchData() { const tasks = [ - fetchAndHandle(`${mempoolBaseUrl}/api/blocks/tip/hash`), - fetchAndHandle(`${esploraBaseUrl}/api/blocks/tip/hash`), - fetchAndHandle(`${mempoolBaseUrl}/api/v1/fees/recommended`), - fetchAndHandle(`${esploraBaseUrl}/api/fee-estimates`) + fetchAndHandle(MEMPOOL_TIP_HASH_URL), + fetchAndHandle(ESPLORA_TIP_HASH_URL), + fetchAndHandle(MEMPOOL_FEES_URL), + fetchAndHandle(ESPLORA_FEE_ESTIMATES_URL) ]; return await Promise.allSettled(tasks); @@ -156,27 +137,24 @@ async function getEstimates() : Promise { return estimates; } +/** + * Helper function to extract value from a fulfilled promise. + */ +function getValueFromFulfilledPromise(result: PromiseSettledResult) { + return result.status === "fulfilled" && result.value ? result.value : null; +} + /** * Assigns the results of the fetch tasks to variables. */ function assignResults(results: PromiseSettledResult[]) { - let blocksTipHash, mempoolFeeEstimates, esploraFeeEstimates; - - if (results[0].status === "fulfilled" && results[0].value) { - blocksTipHash = results[0].value; - } else if (results[1].status === "fulfilled" && results[1].value) { - blocksTipHash = results[1].value; - } + const [result1, result2, result3, result4] = results; - if (results[2].status === "fulfilled" && results[2].value) { - mempoolFeeEstimates = results[2].value as MempoolFeeEstimates; - } - - if (results[3].status === "fulfilled" && results[3].value) { - esploraFeeEstimates = results[3].value as EsploraFeeEstimates; - } + const blocksTipHash = getValueFromFulfilledPromise(result1) || getValueFromFulfilledPromise(result2); + const mempoolFeeEstimates = getValueFromFulfilledPromise(result3) as MempoolFeeEstimates; + const esploraFeeEstimates = getValueFromFulfilledPromise(result4) as EsploraFeeEstimates; - return { blocksTipHash, mempoolFeeEstimates, esploraFeeEstimates: esploraFeeEstimates }; + return { blocksTipHash, mempoolFeeEstimates, esploraFeeEstimates }; } /** @@ -186,6 +164,14 @@ function calculateFees(mempoolFeeEstimates: MempoolFeeEstimates | null | undefin let feeByBlockTarget: FeeByBlockTarget = {}; const minFee = mempoolFeeEstimates?.minimumFee; + feeByBlockTarget = calculateMempoolFees(mempoolFeeEstimates, feeByBlockTarget); + const minMempoolFee = calculateMinMempoolFee(feeByBlockTarget); + feeByBlockTarget = calculateEsploraFees(esploraFeeEstimates, feeByBlockTarget, minMempoolFee, minFee); + + return feeByBlockTarget; +} + +function calculateMempoolFees(mempoolFeeEstimates: MempoolFeeEstimates | null | undefined, feeByBlockTarget: FeeByBlockTarget) { if (mempoolFeeEstimates) { const blockTargetMapping: BlockTargetMapping = { 1: 'fastestFee', @@ -200,24 +186,35 @@ function calculateFees(mempoolFeeEstimates: MempoolFeeEstimates | null | undefin } } } + return feeByBlockTarget; +} +function calculateMinMempoolFee(feeByBlockTarget: FeeByBlockTarget) { const values = Object.values(feeByBlockTarget); - const minMempoolFee = values.length > 0 ? Math.min(...values) : undefined; + return values.length > 0 ? Math.min(...values) : undefined; +} + +function shouldSkipFee(blockTarget: string, adjustedFee: number, feeByBlockTarget: FeeByBlockTarget, minMempoolFee: number | undefined, minFee: number | undefined): boolean { + const blockTargetInt = parseInt(blockTarget); + + if (feeByBlockTarget.hasOwnProperty(blockTarget)) return true; + if (minMempoolFee && adjustedFee >= minMempoolFee) return true; + if (minFee && adjustedFee <= minFee) return true; + if (blockTargetInt <= mempoolDepth) return true; + return false; +} + +function calculateEsploraFees(esploraFeeEstimates: EsploraFeeEstimates | null | undefined, feeByBlockTarget: FeeByBlockTarget, minMempoolFee: number | undefined, minFee: number | undefined) { if (esploraFeeEstimates) { for (const [blockTarget, fee] of Object.entries(esploraFeeEstimates)) { - const blockTargetInt = parseInt(blockTarget); const adjustedFee = Math.round(fee * 1000 * feeMultiplier); - if (feeByBlockTarget.hasOwnProperty(blockTarget)) continue; - if (minMempoolFee && adjustedFee >= minMempoolFee) continue; - if (minFee && adjustedFee <= minFee) continue; - if (blockTargetInt <= mempoolDepth) continue; + if (shouldSkipFee(blockTarget, adjustedFee, feeByBlockTarget, minMempoolFee, minFee)) continue; feeByBlockTarget[blockTarget] = adjustedFee; } } - return feeByBlockTarget; } @@ -246,8 +243,8 @@ const Content = (props: { siteData: SiteData; estimates: Estimates }) => (
- - + +
@@ -266,13 +263,13 @@ const Content = (props: { siteData: SiteData; estimates: Estimates }) => ( overflowX: 'auto' }}> -
+

{props.siteData.title}

{props.siteData.subtitle}

-          curl -L -X GET '{baseUrl}/v1/fee-estimates' -H 'Accept: application/json'
+          curl -L -X GET '{baseUrl}/v1/fee-estimates' -H 'Accept: application/json'
         
@@ -320,7 +317,7 @@ app.get('/', async (c) => {
  */
 app.get('/v1/fee-estimates', async (c) => {
   try {
-    var estimates = await getEstimates();
+    let estimates = await getEstimates();
 
     // Set cache headers.
     c.res.headers.set('Cache-Control', `public, max-age=${stdTTL}`)