Update server/addresses.js

Validate Bitcoin addresses before storing
This commit is contained in:
2026-08-29 11:25:59 +00:00
parent f702b4f367
commit cf3acaa59e
+29 -231
View File
@@ -1,9 +1,11 @@
const crypto = require('crypto')
const fs = require('fs/promises')
const net = require('net')
const {
addressToScriptPubKey,
} = require('./bitcoin-address')
const storePath = '/data/store.json'
const fulcrumTimeoutMs = 3000
async function readStore() {
try {
@@ -57,6 +59,18 @@ function normalizeLabel(label) {
return normalized || undefined
}
function validateBitcoinAddress(address) {
try {
addressToScriptPubKey(address)
} catch (error) {
throw new Error(
error instanceof Error
? error.message
: 'Invalid Bitcoin address',
)
}
}
async function listAddresses() {
const store = await readStore()
@@ -72,10 +86,18 @@ async function addAddress(address, label) {
throw new Error('Bitcoin address is required')
}
validateBitcoinAddress(normalizedAddress)
const addresses = await listAddresses()
if (addresses.some((item) => item.address === normalizedAddress)) {
throw new Error('Bitcoin address is already being watched')
if (
addresses.some(
(item) => item.address === normalizedAddress,
)
) {
throw new Error(
'Bitcoin address is already being watched',
)
}
const entry = {
@@ -114,7 +136,9 @@ async function removeAddress(address) {
)
if (filtered.length === addresses.length) {
throw new Error('Bitcoin address is not being watched')
throw new Error(
'Bitcoin address is not being watched',
)
}
store.addresses = filtered
@@ -124,234 +148,8 @@ async function removeAddress(address) {
return true
}
function queryFulcrum(fulcrumUrl, method, params = []) {
return new Promise((resolve) => {
if (!fulcrumUrl) {
resolve({
ok: false,
result: null,
error: 'Fulcrum URL is not configured',
})
return
}
let parsed
try {
parsed = new URL(fulcrumUrl)
} catch {
resolve({
ok: false,
result: null,
error: 'Invalid Fulcrum URL',
})
return
}
if (parsed.protocol !== 'tcp:') {
resolve({
ok: false,
result: null,
error: 'Fulcrum URL must use tcp://',
})
return
}
const host = parsed.hostname
const port = Number(parsed.port)
if (!host || !port) {
resolve({
ok: false,
result: null,
error: 'Fulcrum URL is missing host or port',
})
return
}
const socket = net.createConnection({
host,
port,
})
let response = ''
let settled = false
const finish = (value) => {
if (settled) {
return
}
settled = true
socket.destroy()
resolve(value)
}
socket.setTimeout(fulcrumTimeoutMs)
socket.on('connect', () => {
const request = JSON.stringify({
jsonrpc: '2.0',
id: 1,
method,
params,
}) + '\n'
socket.write(request)
})
socket.on('data', (data) => {
response += data.toString()
const lines = response.split('\n')
for (const line of lines) {
if (!line.trim()) {
continue
}
let message
try {
message = JSON.parse(line)
} catch {
continue
}
if (message.error) {
finish({
ok: false,
result: null,
error: message.error,
})
return
}
if (
Object.prototype.hasOwnProperty.call(
message,
'result',
)
) {
finish({
ok: true,
result: message.result,
error: null,
})
return
}
}
})
socket.on('timeout', () => {
finish({
ok: false,
result: null,
error: 'Connection timed out',
})
})
socket.on('error', (error) => {
finish({
ok: false,
result: null,
error: error.message,
})
})
socket.on('close', () => {
finish({
ok: false,
result: null,
error: 'Connection closed before a response was received',
})
})
})
}
async function getAddressData(address) {
const fulcrumUrl = process.env.MUNIN_FULCRUM_URL || null
if (!fulcrumUrl) {
throw new Error('Fulcrum URL is not configured')
}
const history = await queryFulcrum(
fulcrumUrl,
'blockchain.address.get_history',
[address],
)
if (!history.ok) {
throw new Error(
`Failed to query address history: ${JSON.stringify(history.error)}`,
)
}
const utxos = await queryFulcrum(
fulcrumUrl,
'blockchain.address.listunspent',
[address],
)
if (!utxos.ok) {
throw new Error(
`Failed to query address UTXOs: ${JSON.stringify(utxos.error)}`,
)
}
const confirmedBalance = utxos.result.reduce(
(total, item) => total + Number(item.value || 0),
0,
)
return {
address,
confirmedBalance,
unconfirmedBalance: 0,
utxos: utxos.result,
history: history.result,
}
}
async function getAddressesWithData() {
const addresses = await listAddresses()
const results = []
for (const entry of addresses) {
try {
const data = await getAddressData(entry.address)
results.push({
...entry,
confirmedBalance: data.confirmedBalance,
unconfirmedBalance: data.unconfirmedBalance,
utxos: data.utxos,
history: data.history,
error: null,
})
} catch (error) {
results.push({
...entry,
confirmedBalance: null,
unconfirmedBalance: null,
utxos: [],
history: [],
error: error.message,
})
}
}
return results
}
module.exports = {
listAddresses,
addAddress,
removeAddress,
getAddressData,
getAddressesWithData,
}