Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'
import { blockCache } from './lib/cache'
import { handleRouteError, parseNetwork, parseLimit, parseOffset, parseBlockOffset, ValidationError } from './lib/http'
import { handleRouteError, parseNetwork, parseLimit, parseOffset, parseBlockOffset, parseAddress, ValidationError } from './lib/http'
import { computeCirculatingSupply, currentBlockReward } from './lib/supply'
import { rpcWithNetwork } from '@fairco.in/rpc-client'
import priceRouter from './routes/price'
Expand Down Expand Up @@ -388,9 +388,8 @@ app.get('/api/mining-info', async (req, res) => {

app.get('/api/validate-address', async (req, res) => {
try {
const address = req.query.address as string
const network = parseNetwork(req.query.network)
if (!address) { res.status(400).json({ error: 'Address parameter is required' }); return }
const address = parseAddress(req.query.address)
const validation = await blockCache.validateAddress(address, network)
res.json(validation)
} catch (error) {
Expand Down
5 changes: 3 additions & 2 deletions server/lib/cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MongoClient, Db } from 'mongodb'
import { rpcWithNetwork, type NetworkType, type RpcParam } from '@fairco.in/rpc-client'
import { escapeRegex, MAX_BLOCK_OFFSET } from './http'
import { escapeRegex, MAX_BLOCK_OFFSET, sanitizeAddressValidation } from './http'

const MONGODB_URI = process.env.MONGODB_URI || 'mongodb://localhost:27017/faircoin-explorer'

Expand Down Expand Up @@ -602,7 +602,8 @@ export class BlockCache extends BlockchainCache {
}

async validateAddress(address: string, network: NetworkType) {
return await this.get<any>('validateaddress', [address], { network, ttl: 86400 }) // 24 hours for address validation
const validation = await this.get<any>('validateaddress', [address], { network, ttl: 86400 }) // 24 hours for address validation
return sanitizeAddressValidation(validation)
}

/**
Expand Down
21 changes: 21 additions & 0 deletions server/lib/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
parseBlockOffset,
parseAddress,
escapeRegex,
sanitizeAddressValidation,
ValidationError,
MIN_LIMIT,
MAX_LIMIT,
Expand Down Expand Up @@ -102,3 +103,23 @@ describe('escapeRegex', () => {
expect(escapeRegex('mainnet:abc123')).toBe('mainnet:abc123')
})
})

describe('sanitizeAddressValidation', () => {
it('keeps only public address validation fields', () => {
expect(sanitizeAddressValidation({
isvalid: true,
address: 'fExampleAddress',
ismine: true,
iswatchonly: true,
isscript: true,
pubkey: 'secret-local-pubkey',
account: 'operator-wallet',
labels: ['operator'],
})).toEqual({ isvalid: true, address: 'fExampleAddress' })
})

it('normalizes malformed RPC responses to an invalid public result', () => {
expect(sanitizeAddressValidation(null)).toEqual({ isvalid: false })
expect(sanitizeAddressValidation({ isvalid: 'yes', address: 42 })).toEqual({ isvalid: false })
})
})
21 changes: 21 additions & 0 deletions server/lib/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ export function parseAddress(value: unknown): string {
return value
}

export interface PublicAddressValidation {
isvalid: boolean
address?: string
}

/**
* Remove wallet-local fields from daemon `validateaddress` output before it is
* exposed by public APIs. Some FairCoin/Bitcoin-family daemons include values
* such as ownership, watch-only state, public keys, labels, or account names.
*/
export function sanitizeAddressValidation(value: unknown): PublicAddressValidation {
const result = value && typeof value === 'object' ? value as Record<string, unknown> : {}
const sanitized: PublicAddressValidation = { isvalid: result.isvalid === true }

if (typeof result.address === 'string') {
sanitized.address = result.address
}

return sanitized
}

/**
* Central error responder. Logs the full error server-side (with context) and
* returns a generic message to the client so internals never leak. When the
Expand Down
21 changes: 0 additions & 21 deletions src/components/address-validator-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,27 +227,6 @@ export function AddressValidatorContent() {
common={common}
highlight
/>
{nodeValidation.ismine !== undefined ? (
<NodeRow
label={t('networkValidation.isMine')}
ok={nodeValidation.ismine}
common={common}
/>
) : null}
{nodeValidation.iswatchonly !== undefined ? (
<NodeRow
label={t('networkValidation.watchOnly')}
ok={nodeValidation.iswatchonly}
common={common}
/>
) : null}
{nodeValidation.isscript !== undefined ? (
<NodeRow
label={t('networkValidation.scriptAddress')}
ok={nodeValidation.isscript}
common={common}
/>
) : null}
</div>
</div>
) : null}
Expand Down
8 changes: 1 addition & 7 deletions src/hooks/use-validate-address.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
import { useQuery, type UseQueryResult } from '@tanstack/react-query'
import { useNetwork } from '@/contexts/network-context'

/**
* Result of the node-side `validateaddress` RPC call exposed by `/api/validate-address`.
* Fields beyond `isvalid` are only present when the node recognises/owns the address.
*/
/** Public, wallet-metadata-free result exposed by `/api/validate-address`. */
export interface NodeAddressValidation {
isvalid: boolean
address?: string
ismine?: boolean
iswatchonly?: boolean
isscript?: boolean
}

/**
Expand Down
Loading