Add persistent server-side address cache
This commit is contained in:
+195
-17
@@ -22,6 +22,133 @@ const {
|
|||||||
const port = 8080
|
const port = 8080
|
||||||
const fulcrumTimeoutMs = 3000
|
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 = []) {
|
function queryFulcrum(fulcrumUrl, method, params = []) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!fulcrumUrl) {
|
if (!fulcrumUrl) {
|
||||||
@@ -247,23 +374,17 @@ async function getStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAddressData(address) {
|
async function getAddressDataFromFulcrum(
|
||||||
let scripthash
|
address,
|
||||||
|
scripthash,
|
||||||
try {
|
) {
|
||||||
scripthash = addressToScripthash(address)
|
const fulcrumUrl =
|
||||||
} catch (error) {
|
process.env.MUNIN_FULCRUM_URL || null
|
||||||
throw new Error(
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: 'Invalid Bitcoin address',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fulcrumUrl = process.env.MUNIN_FULCRUM_URL || null
|
|
||||||
|
|
||||||
if (!fulcrumUrl) {
|
if (!fulcrumUrl) {
|
||||||
throw new Error('Fulcrum URL is not configured')
|
throw new Error(
|
||||||
|
'Fulcrum URL is not configured',
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const balance = await queryFulcrum(
|
const balance = await queryFulcrum(
|
||||||
@@ -309,12 +430,63 @@ async function getAddressData(address) {
|
|||||||
Number(balance.result?.confirmed || 0),
|
Number(balance.result?.confirmed || 0),
|
||||||
unconfirmedBalance:
|
unconfirmedBalance:
|
||||||
Number(balance.result?.unconfirmed || 0),
|
Number(balance.result?.unconfirmed || 0),
|
||||||
utxos: Array.isArray(utxos.result)
|
utxos:
|
||||||
|
Array.isArray(utxos.result)
|
||||||
? utxos.result
|
? utxos.result
|
||||||
: [],
|
: [],
|
||||||
history: Array.isArray(history.result)
|
history:
|
||||||
|
Array.isArray(history.result)
|
||||||
? 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,
|
unconfirmedBalance: data.unconfirmedBalance,
|
||||||
utxos: data.utxos,
|
utxos: data.utxos,
|
||||||
history: data.history,
|
history: data.history,
|
||||||
|
lastSynced:
|
||||||
|
data.lastSynced || null,
|
||||||
|
cacheAgeMs:
|
||||||
|
Number(data.cacheAgeMs || 0),
|
||||||
error: null,
|
error: null,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -342,6 +518,8 @@ async function getAddressesWithData() {
|
|||||||
unconfirmedBalance: null,
|
unconfirmedBalance: null,
|
||||||
utxos: [],
|
utxos: [],
|
||||||
history: [],
|
history: [],
|
||||||
|
lastSynced: null,
|
||||||
|
cacheAgeMs: null,
|
||||||
error:
|
error:
|
||||||
error instanceof Error
|
error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
|
|||||||
Reference in New Issue
Block a user