Skip to content

Commit

Permalink
Merge pull request #24 from LN-Zap/fallback
Browse files Browse the repository at this point in the history
Add support for fallback APIs
  • Loading branch information
mrfelton authored Jan 20, 2024
2 parents 92d9c2b + 39c5f44 commit b565d7b
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 19 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ Here are the available configuration options:
- `server.port`: The port on which the server runs. Default is `3000`
- `server.baseUrl`: The base url port on which the server is accessible. Default is `http://localhost:3000`
- `esplora.baseUrl`: The base URL of the Esplora API instance to connect to. Default is `https://blockstream.info`
- `esplora.fallbacekBaseUrl`: The base URL of the Esplora API instance to fallback to if the primary instance is unavailable.
- `mempool.baseUrl`: The base URL of the Mempool instance to connect to. Default is `https://mempool.space`
- `mempool.fallbacekBaseUrl`: The base URL of the Mempool instance to fallback to if the primary instance is unavailable.
- `mempool.depth`: The number of blocks to use for mempool-based fee estimates. Default is `6`. Valid options are `1`, `3`, and `6`
- `settings.feeMultiplier`: The multiplier to apply to the fee estimates. Default is `1` (a conservative approach to ensure that the fee estimates are always slightly higher than the raw estimates)
- `cache.stdTTL`: The standard time to live in seconds for every generated cache element. Default is `15`
Expand All @@ -80,7 +82,9 @@ In addition to configuring the application through the config files, you can als
- `PORT`: Overrides `server.port`
- `BASE_URL`: Overrides `server.baseUrl`
- `ESPLORA_BASE_URL`: Overrides `esplora.baseUrl`
- `ESPLORA_FALLBACK_BASE_URL`: Overrides `esplora.fallbackBaseUrl`
- `MEMPOOL_BASE_URL`: Overrides `mempool.baseUrl`
- `MEMPOOL_FALLBACK_BASE_URL`: Overrides `mempool.fallbackBaseUrl`
- `MEMPOOL_DEPTH`: Overrides `mempool.depth`
- `FEE_MULTIPLIER`: Overrides `settings.feeMultiplier`
- `CACHE_STDTTL`: Overrides `cache.stdTTL`
Expand Down
4 changes: 3 additions & 1 deletion config/custom-environment-variables.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
},
"mempool": {
"baseUrl": "MEMPOOL_BASE_URL",
"fallbackBaseUrl": "MEMPOOL_FALLBACK_BASE_URL",
"depth": "MEMPOOL_DEPTH"
},
"esplora": {
"baseUrl": "ESPLORA_BASE_URL"
"baseUrl": "ESPLORA_BASE_URL",
"fallbackBaseUrl": "ESPLORA_FALLBACK_BASE_URL"
},
"cache": {
"stdTTL": "CACHE_STD_TTL",
Expand Down
4 changes: 3 additions & 1 deletion config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
},
"mempool": {
"baseUrl": "https://mempool.space",
"fallbackBaseUrl": null,
"depth": 6
},
"esplora": {
"baseUrl": "https://blockstream.info"
"baseUrl": "https://blockstream.info",
"fallbackBaseUrl": null
},
"cache": {
"stdTTL": 15,
Expand Down
4 changes: 3 additions & 1 deletion src/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ interface SiteData {
title: string,
subtitle: string,
children?: any
}
}

type ExpectedResponseType = 'json' | 'text';
70 changes: 54 additions & 16 deletions src/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import NodeCache from 'node-cache';
const port = config.get<number>('server.port');
const baseUrl = config.get<number>('server.baseUrl');
const esploraBaseUrl = config.get<string>('esplora.baseUrl');
const esploraFallbackBaseUrl = config.get<string>('esplora.fallbackBaseUrl');
const mempoolBaseUrl = config.get<string>('mempool.baseUrl');
const mempoolFallbackBaseUrl = config.get<string>('mempool.fallbackBaseUrl');
const mempoolDepth = config.get<number>('mempool.depth');
const feeMultiplier = config.get<number>('settings.feeMultiplier');
const stdTTL = config.get<number>('cache.stdTTL');
Expand All @@ -25,7 +27,9 @@ console.info('---');
console.info(`Using port: ${port}`);
console.info(`Using base URL: ${baseUrl}`);
console.info(`Using Esplora base URL: ${esploraBaseUrl}`);
console.info(`Using Esplora fallback base URL: ${esploraFallbackBaseUrl}`);
console.info(`Using Mempool base URL: ${mempoolBaseUrl}`);
console.info(`Using Mempool fallback base URL: ${mempoolFallbackBaseUrl}`);
console.info(`Using Mempool estimation depth: ${mempoolDepth}`);
console.info(`Using fee multiplier: ${feeMultiplier}`);
console.info(`Using cache stdTTL: ${stdTTL}`);
Expand All @@ -38,6 +42,11 @@ 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`;

const MEMPOOL_TIP_HASH_URL_FALLBACK = `${mempoolFallbackBaseUrl}/api/blocks/tip/hash`;
const ESPLORA_TIP_HASH_URL_FALLBACK = `${esploraFallbackBaseUrl}/api/blocks/tip/hash`;
const MEMPOOL_FEES_URL_FALLBACK = `${mempoolFallbackBaseUrl}/api/v1/fees/recommended`;
const ESPLORA_FEE_ESTIMATES_URL_FALLBACK = `${esploraFallbackBaseUrl}/api/fee-estimates`;

// Initialize the cache.
const cache = new NodeCache({ stdTTL: stdTTL, checkperiod: checkperiod });
const CACHE_KEY = 'estimates';
Expand All @@ -54,28 +63,57 @@ async function fetchWithTimeout(url: string, timeout: number = TIMEOUT): Promise
return Promise.race([fetchPromise, timeoutPromise]) as Promise<Response>;
}


/**
* Fetches data from the given URL and returns the response as a string or object.
*/
async function fetchAndHandle(url: string): Promise<string | object | null> {
async function fetchAndProcess(url: string, expectedResponseType: ExpectedResponseType): Promise<string | object | null> {
const response = await fetchWithTimeout(url, TIMEOUT);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.debug(`Successfully fetched data from ${url}`);

const contentType = response.headers.get("content-type");
if (expectedResponseType === 'json' && contentType?.includes("application/json")) {
return response.json();
} else if (expectedResponseType === 'text' && contentType?.includes("text/plain")) {
const text = await response.text();
const trimmedText = text.trim();
if (trimmedText.includes('\n') || text !== trimmedText) {
throw new Error('Response is not a single text string with no whitespace or newlines');
}
return trimmedText;
} else {
throw new Error(`Unexpected response type. Expected ${expectedResponseType}, but received ${contentType}`);
}
}

/**
* Fetches data from the given URL and returns the response as a string or object.
*/
async function fetchAndHandle(url: string, expectedResponseType: ExpectedResponseType, fallbackUrl?: string): Promise<string | object | null> {
try {
const response = await fetchWithTimeout(url, TIMEOUT);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
const timeout = new Promise((resolve) => setTimeout(resolve, TIMEOUT, 'timeout'));
const fetchPromise = fetchAndProcess(url, expectedResponseType);
const result = await Promise.race([fetchPromise, timeout]) as Promise<Response> | string;

if (result === 'timeout' || result instanceof Error) {
throw new Error('Fetch or timeout error');
}
console.debug(`Successfully fetched data from ${url}`);
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return await response.json();

return result;
} catch (error) {
console.info('Trying fallback URL', fallbackUrl);
if (fallbackUrl) {
return fetchAndProcess(fallbackUrl, expectedResponseType);
} else {
return await response.text();
throw new Error(`Fetch request to ${url} failed and no fallback URL was provided.`);
}
} catch (error) {
console.error(`Error fetching from ${url}:`, error);
return null;
}
}


// Initialize the Express app.
const app = new Hono();
console.info(`Fee Estimates available at ${baseUrl}/v1/fee-estimates`);
Expand Down Expand Up @@ -103,10 +141,10 @@ app.use('/static/*', serveStatic({ root: './' }))
*/
async function fetchData() {
const tasks = [
fetchAndHandle(MEMPOOL_TIP_HASH_URL),
fetchAndHandle(ESPLORA_TIP_HASH_URL),
fetchAndHandle(MEMPOOL_FEES_URL),
fetchAndHandle(ESPLORA_FEE_ESTIMATES_URL)
fetchAndHandle(MEMPOOL_TIP_HASH_URL, 'text', MEMPOOL_TIP_HASH_URL_FALLBACK),
fetchAndHandle(ESPLORA_TIP_HASH_URL, 'text', ESPLORA_TIP_HASH_URL_FALLBACK),
fetchAndHandle(MEMPOOL_FEES_URL, 'json', MEMPOOL_FEES_URL_FALLBACK),
fetchAndHandle(ESPLORA_FEE_ESTIMATES_URL, 'json', ESPLORA_FEE_ESTIMATES_URL_FALLBACK)
];

return await Promise.allSettled(tasks);
Expand Down

0 comments on commit b565d7b

Please sign in to comment.