From 515ecf0ea0c6aace3de55be690d022aca84c8c58 Mon Sep 17 00:00:00 2001 From: Unheard0840 Date: Sat, 29 Aug 2026 15:28:26 +0000 Subject: [PATCH] Update server/status.js Add paginated address transaction API --- server/status.js | 891 ++++++++++++++++++++++++++++++----------------- 1 file changed, 580 insertions(+), 311 deletions(-) diff --git a/server/status.js b/server/status.js index ad0f3f0..3fcb3bb 100644 --- a/server/status.js +++ b/server/status.js @@ -27,6 +27,7 @@ const path = require('path') const addressCacheDir = '/data/cache/addresses' const addressCacheMaxAgeMs = 60000 +const addressTransactionPageSize = 50 const addressRefreshes = new Map() function ensureAddressCacheDir() { @@ -149,7 +150,11 @@ function refreshAddressCache( return refresh } -function queryFulcrum(fulcrumUrl, method, params = []) { +function queryFulcrum( + fulcrumUrl, + method, + params = [], +) { return new Promise((resolve) => { if (!fulcrumUrl) { resolve({ @@ -490,34 +495,89 @@ async function getAddressData(address) { } } +function getAddressTransactionPage( + history, + offset = 0, + limit = addressTransactionPageSize, +) { + const safeOffset = + Math.max( + 0, + Number(offset) || 0, + ) + + const safeLimit = + Math.min( + addressTransactionPageSize, + Math.max( + 1, + Number(limit) || + addressTransactionPageSize, + ), + ) + + return history.slice( + safeOffset, + safeOffset + safeLimit, + ) +} + async function getAddressesWithData() { const addresses = await listAddresses() const results = [] for (const entry of addresses) { try { - const data = await getAddressData(entry.address) + const data = + await getAddressData( + entry.address, + ) + + const history = + Array.isArray(data.history) + ? data.history + : [] results.push({ ...entry, - scripthash: data.scripthash, - confirmedBalance: data.confirmedBalance, - unconfirmedBalance: data.unconfirmedBalance, - utxos: data.utxos, - history: data.history, + scripthash: + data.scripthash, + confirmedBalance: + data.confirmedBalance, + unconfirmedBalance: + data.unconfirmedBalance, + utxos: + data.utxos, + history: + getAddressTransactionPage( + history, + ), + transactionCount: + history.length, + transactionOffset: 0, + transactionLimit: + addressTransactionPageSize, lastSynced: data.lastSynced || null, cacheAgeMs: - Number(data.cacheAgeMs || 0), + Number( + data.cacheAgeMs || 0, + ), error: null, }) } catch (error) { results.push({ ...entry, - confirmedBalance: null, - unconfirmedBalance: null, + confirmedBalance: + null, + unconfirmedBalance: + null, utxos: [], history: [], + transactionCount: 0, + transactionOffset: 0, + transactionLimit: + addressTransactionPageSize, lastSynced: null, cacheAgeMs: null, error: @@ -531,245 +591,362 @@ async function getAddressesWithData() { return results } -const server = http.createServer(async (req, res) => { - if (req.method === 'POST' && req.url === '/api/login') { - let body = '' +const server = http.createServer( + async (req, res) => { + if ( + req.method === 'POST' && + req.url === '/api/login' + ) { + let body = '' - req.on('data', (chunk) => { - body += chunk - }) + req.on('data', (chunk) => { + body += chunk + }) - req.on('end', () => { - try { - const input = JSON.parse(body) + req.on('end', () => { + try { + const input = + JSON.parse(body) - if (!checkPassword(input.password)) { - res.writeHead(401, { - 'Content-Type': 'application/json', + if ( + !checkPassword( + input.password, + ) + ) { + res.writeHead(401, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Invalid password', + }), + ) + + return + } + + const token = + createSession() + + res.writeHead(200, { + 'Content-Type': + 'application/json', + 'Set-Cookie': + 'munin_session=' + + encodeURIComponent(token) + + '; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400', }) res.end( JSON.stringify({ - error: 'Invalid password', + ok: true, }), ) + } catch { + res.writeHead(400, { + 'Content-Type': + 'application/json', + }) - return + res.end( + JSON.stringify({ + error: + 'Invalid request', + }), + ) + } + }) + + return + } + + if (req.url === '/api/status') { + if (!isValidSession(req)) { + res.writeHead(401, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Authentication required', + }), + ) + + return + } + + try { + const status = + await getStatus() + + res.writeHead(200, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify(status), + ) + } catch (error) { + console.error( + 'Failed to get service status:', + error, + ) + + res.writeHead(500, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + service: 'running', + error: + 'Failed to determine service status', + }), + ) + } + + return + } + + if ( + req.method === 'GET' && + req.url === '/api/addresses' + ) { + if (!isValidSession(req)) { + res.writeHead(401, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Authentication required', + }), + ) + + return + } + + try { + const addresses = + await listAddresses() + + res.writeHead(200, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + addresses, + }), + ) + } catch (error) { + console.error( + 'Failed to list addresses:', + error, + ) + + res.writeHead(500, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Failed to list addresses', + }), + ) + } + + return + } + + if ( + req.method === 'GET' && + req.url === '/api/addresses/data' + ) { + if (!isValidSession(req)) { + res.writeHead(401, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Authentication required', + }), + ) + + return + } + + try { + const addresses = + await getAddressesWithData() + + res.writeHead(200, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + addresses, + }), + ) + } catch (error) { + console.error( + 'Failed to get address data:', + error, + ) + + res.writeHead(500, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Failed to get address data', + }), + ) + } + + return + } + + if ( + req.method === 'GET' && + req.url.startsWith( + '/api/addresses/transactions', + ) + ) { + if (!isValidSession(req)) { + res.writeHead(401, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Authentication required', + }), + ) + + return + } + + try { + const url = + new URL( + req.url, + `http://${req.headers.host || 'localhost'}`, + ) + + const address = + url.searchParams.get( + 'address', + ) + + if (!address) { + throw new Error( + 'Address is required', + ) } - const token = createSession() + const offset = + Math.max( + 0, + Number( + url.searchParams.get( + 'offset', + ), + ) || 0, + ) + + const limit = + Math.min( + addressTransactionPageSize, + Math.max( + 1, + Number( + url.searchParams.get( + 'limit', + ), + ) || + addressTransactionPageSize, + ), + ) + + const data = + await getAddressData( + address, + ) + + const history = + Array.isArray( + data.history, + ) + ? data.history + : [] + + const transactions = + getAddressTransactionPage( + history, + offset, + limit, + ) res.writeHead(200, { - 'Content-Type': 'application/json', - 'Set-Cookie': - 'munin_session=' + - encodeURIComponent(token) + - '; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400', + 'Content-Type': + 'application/json', }) res.end( JSON.stringify({ - ok: true, + address, + scripthash: + data.scripthash, + transactions, + total: + history.length, + offset, + limit, + hasMore: + offset + + transactions.length < + history.length, + lastSynced: + data.lastSynced || + null, + cacheAgeMs: + Number( + data.cacheAgeMs || 0, + ), }), ) - } catch { - res.writeHead(400, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: 'Invalid request', - }), - ) - } - }) - - return - } - - if (req.url === '/api/status') { - if (!isValidSession(req)) { - res.writeHead(401, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: 'Authentication required', - }), - ) - - return - } - - try { - const status = await getStatus() - - res.writeHead(200, { - 'Content-Type': 'application/json', - }) - - res.end(JSON.stringify(status)) - } catch (error) { - console.error( - 'Failed to get service status:', - error, - ) - - res.writeHead(500, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - service: 'running', - error: 'Failed to determine service status', - }), - ) - } - - return - } - - if (req.method === 'GET' && req.url === '/api/addresses') { - if (!isValidSession(req)) { - res.writeHead(401, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: 'Authentication required', - }), - ) - - return - } - - try { - const addresses = await listAddresses() - - res.writeHead(200, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - addresses, - }), - ) - } catch (error) { - console.error( - 'Failed to list addresses:', - error, - ) - - res.writeHead(500, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: 'Failed to list addresses', - }), - ) - } - - return - } - - if ( - req.method === 'GET' && - req.url === '/api/addresses/data' - ) { - if (!isValidSession(req)) { - res.writeHead(401, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: 'Authentication required', - }), - ) - - return - } - - try { - const addresses = await getAddressesWithData() - - res.writeHead(200, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - addresses, - }), - ) - } catch (error) { - console.error( - 'Failed to get address data:', - error, - ) - - res.writeHead(500, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: 'Failed to get address data', - }), - ) - } - - return - } - - if ( - req.method === 'POST' && - req.url === '/api/addresses' - ) { - if (!isValidSession(req)) { - res.writeHead(401, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: 'Authentication required', - }), - ) - - return - } - - let body = '' - - req.on('data', (chunk) => { - body += chunk - }) - - req.on('end', async () => { - try { - const input = JSON.parse(body) - - const entry = await addAddress( - input.address, - input.label, - ) - - res.writeHead(201, { - 'Content-Type': 'application/json', - }) - - res.end(JSON.stringify(entry)) } catch (error) { + console.error( + 'Failed to get address transactions:', + error, + ) + res.writeHead(400, { - 'Content-Type': 'application/json', + 'Content-Type': + 'application/json', }) res.end( @@ -777,57 +954,197 @@ const server = http.createServer(async (req, res) => { error: error instanceof Error ? error.message - : 'Failed to add address', + : 'Failed to get address transactions', }), ) } - }) - return - } + return + } - if ( - req.method === 'DELETE' && - req.url === '/api/addresses' - ) { - if (!isValidSession(req)) { - res.writeHead(401, { - 'Content-Type': 'application/json', + if ( + req.method === 'POST' && + req.url === '/api/addresses' + ) { + if (!isValidSession(req)) { + res.writeHead(401, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Authentication required', + }), + ) + + return + } + + let body = '' + + req.on('data', (chunk) => { + body += chunk }) - res.end( - JSON.stringify({ - error: 'Authentication required', - }), + req.on( + 'end', + async () => { + try { + const input = + JSON.parse(body) + + const entry = + await addAddress( + input.address, + input.label, + ) + + res.writeHead(201, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify(entry), + ) + } catch (error) { + res.writeHead(400, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + error instanceof Error + ? error.message + : 'Failed to add address', + }), + ) + } + }, ) return } - let body = '' - - req.on('data', (chunk) => { - body += chunk - }) - - req.on('end', async () => { - try { - const input = JSON.parse(body) - - await removeAddress(input.address) - - res.writeHead(200, { - 'Content-Type': 'application/json', + if ( + req.method === 'DELETE' && + req.url === '/api/addresses' + ) { + if (!isValidSession(req)) { + res.writeHead(401, { + 'Content-Type': + 'application/json', }) res.end( JSON.stringify({ - ok: true, + error: + 'Authentication required', }), ) + + return + } + + let body = '' + + req.on('data', (chunk) => { + body += chunk + }) + + req.on( + 'end', + async () => { + try { + const input = + JSON.parse(body) + + await removeAddress( + input.address, + ) + + res.writeHead(200, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + ok: true, + }), + ) + } catch (error) { + res.writeHead(400, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + error instanceof Error + ? error.message + : 'Failed to remove address', + }), + ) + } + }, + ) + + return + } + + if ( + req.method === 'GET' && + req.url.startsWith( + '/api/transactions/', + ) + ) { + if (!isValidSession(req)) { + res.writeHead(401, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify({ + error: + 'Authentication required', + }), + ) + + return + } + + const txid = + decodeURIComponent( + req.url.slice( + '/api/transactions/'.length, + ), + ) + + try { + const transaction = + await getTransaction(txid) + + res.writeHead(200, { + 'Content-Type': + 'application/json', + }) + + res.end( + JSON.stringify( + transaction, + ), + ) } catch (error) { res.writeHead(400, { - 'Content-Type': 'application/json', + 'Content-Type': + 'application/json', }) res.end( @@ -835,66 +1152,18 @@ const server = http.createServer(async (req, res) => { error: error instanceof Error ? error.message - : 'Failed to remove address', + : 'Failed to get transaction', }), ) } - }) - - return - } - - if ( - req.method === 'GET' && - req.url.startsWith('/api/transactions/') - ) { - if (!isValidSession(req)) { - res.writeHead(401, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: 'Authentication required', - }), - ) return } - const txid = decodeURIComponent( - req.url.slice('/api/transactions/'.length), - ) - - try { - const transaction = await getTransaction(txid) - - res.writeHead(200, { - 'Content-Type': 'application/json', - }) - - res.end(JSON.stringify(transaction)) - } catch (error) { - res.writeHead(400, { - 'Content-Type': 'application/json', - }) - - res.end( - JSON.stringify({ - error: - error instanceof Error - ? error.message - : 'Failed to get transaction', - }), - ) - } - - return - } - - res.writeHead(404) - res.end('Not found') -}) + res.writeHead(404) + res.end('Not found') + }, +) server.listen(port, () => { console.log(