diff --git a/server/status.js b/server/status.js index 4aa9797..ad0f3f0 100644 --- a/server/status.js +++ b/server/status.js @@ -22,6 +22,133 @@ const { const port = 8080 const fulcrumTimeoutMs = 3000 +const fs = require('fs') +const path = require('path') + +const addressCacheDir = '/data/cache/addresses' +const addressCacheMaxAgeMs = 60000 +const addressRefreshes = new Map() + +function ensureAddressCacheDir() { + fs.mkdirSync(addressCacheDir, { + recursive: true, + }) +} + +function getAddressCachePath(scripthash) { + return path.join( + addressCacheDir, + `${scripthash}.json`, + ) +} + +function readAddressCache(scripthash) { + try { + const cachePath = + getAddressCachePath(scripthash) + + const stat = fs.statSync(cachePath) + + const data = + JSON.parse( + fs.readFileSync( + cachePath, + 'utf8', + ), + ) + + if ( + !data || + typeof data !== 'object' || + !data.address + ) { + return null + } + + return { + data, + ageMs: Math.max( + 0, + Date.now() - stat.mtimeMs, + ), + } + } catch { + return null + } +} + +function writeAddressCache(data) { + try { + ensureAddressCacheDir() + + const cachePath = + getAddressCachePath( + data.scripthash, + ) + + const tempPath = + `${cachePath}.${process.pid}.tmp` + + fs.writeFileSync( + tempPath, + JSON.stringify(data), + 'utf8', + ) + + fs.renameSync( + tempPath, + cachePath, + ) + } catch (error) { + console.error( + 'Failed to write address cache:', + error, + ) + } +} + +function refreshAddressCache( + address, + scripthash, +) { + const existing = + addressRefreshes.get(scripthash) + + if (existing) { + return existing + } + + const refresh = + getAddressDataFromFulcrum( + address, + scripthash, + ) + .then((data) => { + writeAddressCache(data) + return data + }) + .catch((error) => { + console.error( + `Failed to refresh address ${address}:`, + error, + ) + + throw error + }) + .finally(() => { + addressRefreshes.delete( + scripthash, + ) + }) + + addressRefreshes.set( + scripthash, + refresh, + ) + + return refresh +} + function queryFulcrum(fulcrumUrl, method, params = []) { return new Promise((resolve) => { if (!fulcrumUrl) { @@ -247,23 +374,17 @@ async function getStatus() { } } -async function getAddressData(address) { - let scripthash - - try { - scripthash = addressToScripthash(address) - } catch (error) { - throw new Error( - error instanceof Error - ? error.message - : 'Invalid Bitcoin address', - ) - } - - const fulcrumUrl = process.env.MUNIN_FULCRUM_URL || null +async function getAddressDataFromFulcrum( + address, + scripthash, +) { + const fulcrumUrl = + process.env.MUNIN_FULCRUM_URL || null if (!fulcrumUrl) { - throw new Error('Fulcrum URL is not configured') + throw new Error( + 'Fulcrum URL is not configured', + ) } const balance = await queryFulcrum( @@ -309,12 +430,63 @@ async function getAddressData(address) { Number(balance.result?.confirmed || 0), unconfirmedBalance: Number(balance.result?.unconfirmed || 0), - utxos: Array.isArray(utxos.result) - ? utxos.result - : [], - history: Array.isArray(history.result) - ? history.result - : [], + utxos: + Array.isArray(utxos.result) + ? utxos.result + : [], + history: + Array.isArray(history.result) + ? history.result + : [], + lastSynced: + new Date().toISOString(), + } +} + +async function getAddressData(address) { + let scripthash + + try { + scripthash = + addressToScripthash(address) + } catch (error) { + throw new Error( + error instanceof Error + ? error.message + : 'Invalid Bitcoin address', + ) + } + + const cached = + readAddressCache(scripthash) + + if (cached) { + if ( + cached.ageMs > + addressCacheMaxAgeMs + ) { + refreshAddressCache( + address, + scripthash, + ).catch(() => {}) + } + + return { + ...cached.data, + cacheAgeMs: + cached.ageMs, + } + } + + const data = + await refreshAddressCache( + address, + scripthash, + ) + + return { + ...data, + cacheAgeMs: 0, } } @@ -333,6 +505,10 @@ async function getAddressesWithData() { unconfirmedBalance: data.unconfirmedBalance, utxos: data.utxos, history: data.history, + lastSynced: + data.lastSynced || null, + cacheAgeMs: + Number(data.cacheAgeMs || 0), error: null, }) } catch (error) { @@ -342,6 +518,8 @@ async function getAddressesWithData() { unconfirmedBalance: null, utxos: [], history: [], + lastSynced: null, + cacheAgeMs: null, error: error instanceof Error ? error.message