rss
diff --git a/pages/bitcoin-config-generator/index.js b/pages/bitcoin-config-generator/index.js
new file mode 100644
index 000000000..602f05352
--- /dev/null
+++ b/pages/bitcoin-config-generator/index.js
@@ -0,0 +1,902 @@
+import Layout from '@/components/layout'
+import { Accordion, Button, Col, Form, InputGroup, Row } from 'react-bootstrap'
+import { useCallback, useEffect, useMemo, useState } from 'react'
+import { CopyButton } from '@/components/form'
+import Seo from '@/components/seo'
+import { useToast } from '@/components/toast'
+import { getGetServerSideProps } from '@/api/ssrApollo'
+import {
+ CATEGORIES,
+ getInitialState,
+ getDefaultPathForOption,
+ isPathDefaultValue,
+ OPTIONS_BY_KEY,
+ OS_PATH_OPTION_KEYS,
+ SELECT_OPTIONS
+} from './options-data'
+
+export const getServerSideProps = getGetServerSideProps({ query: null })
+
+const CONFIG_PATHS = {
+ windows: '%APPDATA%\\Bitcoin\\bitcoin.conf',
+ mac: '~/Library/Application Support/Bitcoin/bitcoin.conf',
+ linux: '~/.bitcoin/bitcoin.conf'
+}
+
+const OS_OPTIONS = [
+ { value: 'windows', label: 'Windows' },
+ { value: 'mac', label: 'Mac OS' },
+ { value: 'linux', label: 'Linux' }
+]
+
+// Readable in both light and dark mode (avoid text-muted which can be too low-contrast in light theme)
+const secondaryTextStyle = { color: 'var(--bs-body-color)', opacity: 0.85 }
+
+// Inline code: theme-aware, no pink; fits body text
+const inlineCodeStyle = {
+ fontFamily: 'var(--bs-font-monospace)',
+ fontSize: '0.9em',
+ color: 'var(--bs-body-color)',
+ backgroundColor: 'var(--bs-secondary-bg)',
+ padding: '0.15em 0.4em',
+ borderRadius: '0.25rem'
+}
+
+function getConfigPathForOS (os) {
+ return CONFIG_PATHS[os] ?? null
+}
+
+function detectOS () {
+ if (typeof navigator === 'undefined') return null
+ const ua = navigator.userAgent
+ const platform = navigator.userAgentData?.platform ?? navigator.platform ?? ''
+ if (/Win(dows|32|64|16)/i.test(ua) || /Windows/i.test(platform)) return 'windows'
+ if (/Mac|iPhone|iPad/i.test(platform) || /Mac/i.test(ua)) return 'mac'
+ if (/Linux|Android/i.test(ua) || /Linux/i.test(platform)) return 'linux'
+ return null
+}
+
+/** Normalize value for comparison with default */
+function norm (val, opt) {
+ if (val === undefined || val === null || val === '') return ''
+ if (opt.widget === 'switch') {
+ if (val === true || val === 1 || val === '1') return '1'
+ return '0'
+ }
+ if (opt.widget === 'multi_switch') {
+ const s = String(val).trim()
+ if (!s) return ''
+ return s.split(',').map(x => x.trim()).filter(Boolean).sort().join(',')
+ }
+ if (opt.widget === 'number') {
+ const n = Number(val)
+ return isNaN(n) ? '' : String(n)
+ }
+ if (opt.widget === 'number_float_8') {
+ const n = parseFloat(String(val))
+ return isNaN(n) ? '' : String(n)
+ }
+ return String(val).trim()
+}
+
+/** True if current value differs from default (so we should include in conf) */
+function differs (optionKey, value, opt) {
+ const def = opt.default
+ const v = norm(value, opt)
+ const d = norm(opt.widget === 'switch' ? (def === '1' || def === 'true') : def, opt)
+ if (v === d) return false
+ if (v === '' && d === '') return false
+ return true
+}
+
+/** Split multi-line/comma list and filter empty */
+function splitMulti (text) {
+ if (text == null || text === '') return []
+ return String(text).split(/[\n,]/).map(s => s.trim()).filter(Boolean)
+}
+
+function buildConfig (opts, configPathForComment, effectiveOS = null) {
+ const lines = []
+ lines.push('# Generated by Stacker News Bitcoin config Generator')
+ lines.push('# https://stacker.news/bitcoin-config-generator')
+ lines.push('')
+ lines.push('# This config should be placed in following path:')
+ if (configPathForComment) {
+ lines.push(`# ${configPathForComment}`)
+ } else {
+ lines.push('# Linux: ~/.bitcoin/bitcoin.conf')
+ lines.push('# macOS: ~/Library/Application Support/Bitcoin/bitcoin.conf')
+ lines.push('# Windows: %APPDATA%\\Bitcoin\\bitcoin.conf')
+ }
+ lines.push('')
+
+ const push = (key, lineStr, comment) => {
+ const opt = OPTIONS_BY_KEY[key]
+ if (opt && comment !== false) lines.push(`# ${opt.label}`)
+ lines.push(lineStr)
+ }
+
+ const emitOption = (key, opt, optWithDefault, val) => {
+ if (opt.widget === 'multi_switch') {
+ const items = (val && String(val).trim()) ? String(val).split(',').map(s => s.trim()).filter(Boolean) : []
+ if (items.length > 0) {
+ push(key, `${key}=${items[0]}`)
+ for (let i = 1; i < items.length; i++) lines.push(`${key}=${items[i]}`)
+ }
+ return
+ }
+ if (opt.widget === 'switch') {
+ const on = val === true || val === 1 || val === '1'
+ push(key, `${key}=${on ? '1' : '0'}`)
+ return
+ }
+ if (opt.widget === 'textarea') {
+ let items = splitMulti(val)
+ const defaultItems = splitMulti(opt.default)
+ if (items.length === 0 && defaultItems.length === 0) return
+ if (items.length === defaultItems.length && items.every((v, i) => v === defaultItems[i])) return
+ // Bitcoin Core uses at most 8 addnode connections
+ if (key === 'addnode' && items.length > 8) {
+ lines.push('# Bitcoin Core uses at most 8 addnode connections.')
+ items = items.slice(0, 8)
+ }
+ if (items.length > 0) {
+ push(key, `${key}=${items[0]}`)
+ for (let i = 1; i < items.length; i++) lines.push(`${key}=${items[i]}`)
+ }
+ return
+ }
+ if (opt.widget === 'number' || opt.widget === 'number_float_8') {
+ const n = opt.widget === 'number' ? Number(val) : parseFloat(val)
+ if (val !== '' && val != null && !isNaN(n)) {
+ if (opt.widget === 'number_float_8' && n < 0) return
+ // Bitcoin Core prune: only 0, 1, or ≥550 are valid; 2–549 are invalid (treat as manual)
+ let emitVal = key === 'prune' && n > 1 && n < 550 ? 1 : val
+ // Core enforces minimum 1 for timeout and peertimeout
+ if ((key === 'timeout' || key === 'peertimeout') && n < 1) emitVal = 1
+ // Core dbcrashratio is 0–1
+ if (opt.widget === 'number_float_8' && opt.max != null && n > Number(opt.max)) emitVal = Number(opt.max)
+ // Clamp to opt.min/opt.max so emitted config is valid (e.g. URL or preset out-of-range)
+ let numEmit = opt.widget === 'number' ? Number(emitVal) : parseFloat(emitVal)
+ if (!isNaN(numEmit)) {
+ if (opt.min != null && numEmit < Number(opt.min)) numEmit = Number(opt.min)
+ if (opt.max != null && numEmit > Number(opt.max)) numEmit = Number(opt.max)
+ push(key, `${key}=${numEmit}`)
+ }
+ }
+ return
+ }
+ if (val !== '' && val != null && String(val).trim() !== '') {
+ push(key, `${key}=${val}`)
+ }
+ }
+
+ let firstSection = true
+ for (const cat of CATEGORIES) {
+ const optionKeysWithChanges = []
+ for (const key of cat.keys) {
+ const opt = OPTIONS_BY_KEY[key]
+ if (!opt) continue
+ const pathDefault = OS_PATH_OPTION_KEYS.includes(key) && effectiveOS ? getDefaultPathForOption(key, effectiveOS) : null
+ const effectiveDefault = pathDefault !== null ? pathDefault : opt.default
+ const optWithDefault = effectiveDefault !== opt.default ? { ...opt, default: effectiveDefault } : opt
+ // Treat any path-default value as matching current OS so we don't include them (avoids flash on load/OS change)
+ // Treat empty select as option default so we don't include when "use default" is intended
+ const rawVal = opts[key]
+ const valueToCompare = pathDefault !== null && isPathDefaultValue(key, rawVal)
+ ? pathDefault
+ : (opt.widget === 'select' && (rawVal === '' || rawVal == null) && opt.default !== '' ? opt.default : rawVal)
+ if (differs(key, valueToCompare, optWithDefault)) optionKeysWithChanges.push({ key, opt, optWithDefault })
+ }
+ if (optionKeysWithChanges.length === 0) continue
+
+ if (!firstSection) lines.push('')
+ firstSection = false
+ lines.push(`# [${cat.title}]`)
+ for (const { key, opt, optWithDefault } of optionKeysWithChanges) {
+ // -test= only applies to regtest; omit and comment when chain is not regtest
+ if (key === 'test' && opts[key] && String(opts[key]).trim() && opts.chain !== 'regtest') {
+ lines.push('# -test= only applies to regtest; omitted for current chain.')
+ continue
+ }
+ // acceptstalefeeestimates only applies to regtest; omit when chain is not regtest
+ if (key === 'acceptstalefeeestimates' && (opts[key] === true || opts[key] === 1 || opts[key] === '1') && opts.chain !== 'regtest') {
+ lines.push('# acceptstalefeeestimates only applies to regtest; omitted for current chain.')
+ continue
+ }
+ emitOption(key, opt, optWithDefault, opts[key])
+ }
+ }
+
+ if (!firstSection) {
+ lines.push('')
+ lines.push('# Network-specific sections')
+ lines.push('# By default, the options above apply to every chain (main, test, testnet4, signet, regtest).')
+ lines.push('# To restrict an option to one network, place it under the corresponding [section] below.')
+ lines.push('# addnode, connect, port, bind, rpcport, rpcbind and wallet are mainnet-only unless')
+ lines.push('# you repeat them under another section.')
+ lines.push('')
+ lines.push('# Mainnet')
+ lines.push('[main]')
+ lines.push('')
+ lines.push('# Testnet3')
+ lines.push('[test]')
+ lines.push('')
+ lines.push('# Testnet4')
+ lines.push('[testnet4]')
+ lines.push('')
+ lines.push('# Signet')
+ lines.push('[signet]')
+ lines.push('')
+ lines.push('# Regtest')
+ lines.push('[regtest]')
+ lines.push('')
+ }
+
+ return lines.join('\n')
+}
+
+function serializeOptsForUrl (opts, targetOS = null) {
+ const o = {}
+ Object.keys(opts).forEach(k => {
+ const v = opts[k]
+ if (v === DEFAULT_OPTIONS[k]) return
+ if (k !== 'predefinedPreset' && (v === '' || v === null || v === undefined)) return
+ o[k] = v
+ })
+ if (targetOS !== null && targetOS !== undefined && targetOS !== '') o.targetOS = targetOS
+ return btoa(unescape(encodeURIComponent(JSON.stringify(o))))
+}
+
+function deserializeOptsFromUrl (hash) {
+ if (!hash || !hash.startsWith('#')) return null
+ try {
+ const json = decodeURIComponent(escape(atob(hash.slice(1))))
+ const o = JSON.parse(json)
+ const targetOS = o.targetOS
+ const { targetOS: _dropped, ...rest } = { ...getInitialState(), ...o }
+ return { opts: rest, targetOS }
+ } catch {
+ return null
+ }
+}
+
+const DEFAULT_OPTIONS = { ...getInitialState(), predefinedPreset: '' }
+
+// Presets mirror node classes from Bitcoin Core Config Generator (jlopp.github.io/bitcoin-core-config-generator)
+const PRESET_KEYS = ['', 'mining', 'ports', 'lowbandwidth', 'pruned', 'raspberrypi', 'tor', 'testnet', 'regtest', 'nonsync', 'lightningeclair']
+
+const PRESETS = {
+ '': { label: 'Defaults', opts: { ...DEFAULT_OPTIONS, predefinedPreset: '' } },
+ mining: {
+ label: 'Mining',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'mining',
+ dbcache: 4000,
+ maxorphantx: 1000,
+ maxmempool: 1000,
+ blockreconstructionextratxn: 1000,
+ maxsigcachesize: 100,
+ blockmaxweight: 4000000,
+ peerbloomfilters: false
+ }
+ },
+ ports: {
+ label: 'Non-standard Ports',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'ports',
+ port: 8333,
+ rpcport: 8332
+ }
+ },
+ lowbandwidth: {
+ label: 'Low Bandwidth',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'lowbandwidth',
+ blocksonly: true,
+ maxconnections: 20,
+ maxuploadtarget: 500
+ }
+ },
+ pruned: {
+ label: 'Low Disk Usage (Pruned)',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'pruned',
+ prune: 550,
+ dbcache: 2000,
+ persistmempool: true
+ }
+ },
+ raspberrypi: {
+ label: 'Raspberry Pi',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'raspberrypi',
+ prune: 550,
+ dbcache: 100,
+ maxorphantx: 10,
+ maxmempool: 50,
+ maxconnections: 40,
+ maxuploadtarget: 5000
+ }
+ },
+ tor: {
+ label: 'Privacy (Tor)',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'tor',
+ onlynet: 'onion',
+ listenonion: true,
+ listen: false,
+ proxy: '127.0.0.1:9050',
+ onion: '127.0.0.1:9050',
+ dns: false,
+ dnsseed: false,
+ torcontrol: '127.0.0.1:9051'
+ }
+ },
+ testnet: {
+ label: 'Testnet',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'testnet',
+ chain: 'testnet4',
+ rpcport: 18332,
+ dbcache: 1000,
+ logips: true
+ }
+ },
+ regtest: {
+ label: 'Regtest',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'regtest',
+ chain: 'regtest',
+ rpcport: 18443,
+ dbcache: 500,
+ logips: true,
+ logtimemicros: true
+ }
+ },
+ nonsync: {
+ label: 'Non-syncing',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'nonsync',
+ listen: false,
+ maxconnections: 0,
+ networkactive: false
+ }
+ },
+ lightningeclair: {
+ label: 'Lightning (Eclair)',
+ opts: {
+ ...DEFAULT_OPTIONS,
+ predefinedPreset: 'lightningeclair',
+ txindex: true,
+ server: true,
+ zmqpubhashtx: 'tcp://127.0.0.1:29000',
+ zmqpubrawblock: 'tcp://127.0.0.1:29000'
+ }
+ }
+}
+
+const FLOAT_STEP = 0.00000001
+
+function OptionField ({ optionKey, opts, update, effectiveOS = null }) {
+ const opt = OPTIONS_BY_KEY[optionKey]
+ if (!opt) return null
+ const val = opts[optionKey]
+ const { label, description, widget } = opt
+
+ const setVal = (v) => update(optionKey, v)
+
+ if (widget === 'switch') {
+ const checked = val === true || val === 1 || val === '1'
+ const acceptStaleOnlyRegtest = optionKey === 'acceptstalefeeestimates' && checked && opts.chain !== 'regtest'
+ const pruneTxindexConflict = optionKey === 'txindex' && checked && (Number(opts.prune) > 0 || opts.prune === true || opts.prune === '1')
+ const listenListenonionConflict = (optionKey === 'listen' && !checked && (opts.listenonion === true || opts.listenonion === 1 || opts.listenonion === '1')) ||
+ (optionKey === 'listenonion' && checked && !(opts.listen === true || opts.listen === 1 || opts.listen === '1'))
+ const forcednsseedDnsseedConflict = (optionKey === 'forcednsseed' && checked && !(opts.dnsseed === true || opts.dnsseed === 1 || opts.dnsseed === '1')) ||
+ (optionKey === 'dnsseed' && !checked && (opts.forcednsseed === true || opts.forcednsseed === 1 || opts.forcednsseed === '1'))
+ return (
+
+ setVal(e.target.checked)}
+ />
+ {description && {description}}
+ {acceptStaleOnlyRegtest && (
+
+ acceptstalefeeestimates only applies to regtest. Current chain is {opts.chain || 'main'}; this option will be omitted in the generated config.
+
+ )}
+ {pruneTxindexConflict && (
+
+ Prune mode is incompatible with txindex in Bitcoin Core.
+
+ )}
+ {listenListenonionConflict && (
+
+ Bitcoin Core errors: cannot set listen=0 with listenonion=1.
+
+ )}
+ {forcednsseedDnsseedConflict && (
+
+ Bitcoin Core errors: cannot set forcednsseed when dnsseed is false.
+
+ )}
+
+ )
+ }
+
+ if (widget === 'multi_switch') {
+ const enabledSet = new Set((val && String(val)) ? String(val).split(',').map(s => s.trim()).filter(Boolean) : [])
+ const options = opt.multiSwitchOptions || []
+ const toggle = (value, checked) => {
+ const next = new Set(enabledSet)
+ if (checked) next.add(value)
+ else next.delete(value)
+ setVal([...next].sort().join(','))
+ }
+ return (
+
+ {label}
+ {description && {description}}
+ {options.map((o, i) => (
+ toggle(o.value, e.target.checked)}
+ />
+ ))}
+
+ )
+ }
+
+ if (widget === 'select') {
+ const options = SELECT_OPTIONS[optionKey] || []
+ const current = (val !== '' && val != null) ? val : opt.default
+ const testOnlyRegtest = optionKey === 'test' && current && opts.chain !== 'regtest'
+ return (
+
+ {label}
+ {description && {description}}
+ {testOnlyRegtest && (
+
+ -test= only applies to regtest. Current chain is {opts.chain || 'main'}; this option will be omitted in the generated config.
+
+ )}
+ setVal(e.target.value)}>
+ {options.map((o, i) => (
+
+ ))}
+
+
+ )
+ }
+
+ if (widget === 'textarea') {
+ return (
+
+ {label}
+ {description && {description}}
+ setVal(e.target.value)}
+ placeholder={optionKey}
+ className='font-monospace small'
+ />
+
+ )
+ }
+
+ if (widget === 'number') {
+ const minVal = (opt.min != null && opt.min !== '') ? Number(opt.min) : 0
+ const maxVal = (opt.max != null && opt.max !== '') ? Number(opt.max) : undefined
+ const allowNegative = minVal < 0
+ const displayVal = (() => {
+ if (val === '' || (val === '-' && allowNegative)) return val
+ if (typeof val === 'string') return val
+ const n = typeof val === 'number' ? val : parseInt(val, 10)
+ return Math.max(minVal, maxVal != null ? Math.min(maxVal, n) : n)
+ })()
+ const numericDisplay = displayVal === '' || displayVal === '-' ? (allowNegative ? minVal : 0) : Number(displayVal)
+ const step = 1
+ return (
+
+ {label}
+ {description && {description}}
+ {optionKey === 'prune' && !isNaN(numericDisplay) && numericDisplay > 1 && numericDisplay < 550 && (
+
+ Values 2–549 are stored as 1 (manual pruning) in the generated config.
+
+ )}
+ {optionKey === 'prune' && !isNaN(numericDisplay) && numericDisplay > 0 && (opts.txindex === true || opts.txindex === 1 || opts.txindex === '1') && (
+
+ Prune mode is incompatible with txindex in Bitcoin Core.
+
+ )}
+
+
+ {
+ const v = e.target.value
+ if (!/^-?\d*$/.test(v)) return
+ if (v === '' || (v === '-' && allowNegative)) { setVal(v); return }
+ const n = parseInt(v, 10)
+ if (!isNaN(n) && n >= minVal && (maxVal == null || n <= maxVal)) setVal(n)
+ else setVal(v)
+ }}
+ onKeyDown={e => {
+ if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ const next = displayVal === '' || displayVal === '-' ? minVal : numericDisplay + step
+ setVal(maxVal != null ? Math.min(maxVal, next) : next)
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ const next = displayVal === '' || displayVal === '-' ? minVal : Math.max(minVal, numericDisplay - step)
+ setVal(maxVal != null ? Math.min(maxVal, next) : next)
+ }
+ }}
+ placeholder={optionKey}
+ className='font-monospace small'
+ />
+
+
+
+ )
+ }
+
+ if (widget === 'number_float_8') {
+ const maxFloat = (opt.max != null && opt.max !== '') ? Number(opt.max) : undefined
+ const numVal = val === '' || val == null ? '' : (typeof val === 'number' ? val : parseFloat(val))
+ const displayVal = (() => {
+ if (val === '' || val === '.' || (typeof val === 'string' && /\.$/.test(val))) return val
+ if (numVal === '' || isNaN(numVal)) return val
+ let v = Math.max(0, numVal)
+ if (maxFloat != null && !isNaN(maxFloat) && v > maxFloat) v = maxFloat
+ return v
+ })()
+ const step = FLOAT_STEP
+ const numForStep = displayVal === '' || displayVal === '.' || isNaN(parseFloat(displayVal)) ? 0 : parseFloat(displayVal)
+ const clamp = (n) => {
+ let x = Math.max(0, n)
+ if (maxFloat != null && !isNaN(maxFloat) && x > maxFloat) x = maxFloat
+ return x
+ }
+ return (
+
+ {label}
+ {description && {description}}
+
+
+ {
+ const v = e.target.value
+ if (!/^\d*\.?\d{0,8}$/.test(v)) return
+ if (v === '') { setVal(''); return }
+ if (v === '.' || /\.$/.test(v)) { setVal(v); return }
+ const n = parseFloat(v)
+ if (!isNaN(n) && n >= 0) setVal(clamp(n))
+ else setVal(v)
+ }}
+ onKeyDown={e => {
+ if (e.key === 'ArrowUp') {
+ e.preventDefault()
+ const next = clamp(numForStep + step)
+ setVal(Number.isInteger(next) ? next : parseFloat(next.toFixed(8)))
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault()
+ const next = clamp(numForStep - step)
+ setVal(Number.isInteger(next) ? next : parseFloat(next.toFixed(8)))
+ }
+ }}
+ placeholder={optionKey}
+ className='font-monospace small'
+ />
+
+
+
+ )
+ }
+
+ const isPassword = widget === 'password'
+ const pathPlaceholder = OS_PATH_OPTION_KEYS.includes(optionKey) && effectiveOS ? getDefaultPathForOption(optionKey, effectiveOS) : ''
+ const placeholder = pathPlaceholder || optionKey
+ return (
+
+ {label}
+ {description && {description}}
+ setVal(e.target.value)}
+ placeholder={placeholder}
+ className='font-monospace small'
+ />
+
+ )
+}
+
+export default function BitcoinconfigGenerator () {
+ const [opts, setOpts] = useState(DEFAULT_OPTIONS)
+ const [detectedOS, setDetectedOS] = useState(null)
+ const [selectedOS, setSelectedOS] = useState(null)
+ const [shareUrl, setShareUrl] = useState('')
+ const toaster = useToast()
+
+ useEffect(() => {
+ const os = detectOS()
+ setDetectedOS(os)
+ setSelectedOS(prev => prev ?? os ?? 'linux')
+ }, [])
+
+ const effectiveOS = selectedOS ?? detectedOS ?? 'linux'
+ const pathForDisplay = getConfigPathForOS(selectedOS ?? detectedOS ?? 'linux')
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return
+ const loaded = deserializeOptsFromUrl(window.location.hash)
+ if (loaded) {
+ setOpts(loaded.opts)
+ if (loaded.targetOS != null && loaded.targetOS !== '') setSelectedOS(loaded.targetOS)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (typeof window === 'undefined') return
+ const encoded = serializeOptsForUrl(opts, selectedOS)
+ const url = encoded ? `${window.location.pathname}#${encoded}` : window.location.pathname
+ setShareUrl(url)
+ }, [opts, selectedOS])
+
+ // When OS is known or changes, set path options (blocksdir, datadir, etc.) to the correct default for that OS
+ useEffect(() => {
+ if (!effectiveOS) return
+ setOpts(prev => {
+ let changed = false
+ const next = { ...prev }
+ for (const key of OS_PATH_OPTION_KEYS) {
+ if (isPathDefaultValue(key, prev[key])) {
+ const pathDefault = getDefaultPathForOption(key, effectiveOS)
+ if (prev[key] !== pathDefault) {
+ next[key] = pathDefault
+ changed = true
+ }
+ }
+ }
+ return changed ? next : prev
+ })
+ }, [effectiveOS])
+
+ const update = useCallback((key, value) => {
+ setOpts(prev => ({
+ ...prev,
+ [key]: value,
+ predefinedPreset: key === 'predefinedPreset' ? value : ''
+ }))
+ }, [])
+
+ const applyPreset = useCallback((key) => {
+ if (key === undefined || key === null || !PRESETS[key]) return
+ const presetOpts = PRESETS[key].opts
+ const next = { ...presetOpts }
+ if (effectiveOS) {
+ for (const k of OS_PATH_OPTION_KEYS) {
+ if (next[k] === '' || isPathDefaultValue(k, next[k])) {
+ next[k] = getDefaultPathForOption(k, effectiveOS)
+ }
+ }
+ }
+ setOpts(next)
+ }, [effectiveOS])
+
+ const config = useMemo(() => buildConfig(opts, pathForDisplay, effectiveOS), [opts, pathForDisplay, effectiveOS])
+
+ const handleSave = useCallback(async () => {
+ try {
+ if ('showSaveFilePicker' in window) {
+ const handle = await window.showSaveFilePicker({
+ suggestedName: 'bitcoin.conf',
+ types: [{ description: 'Bitcoin config', accept: { 'text/plain': ['.conf'] } }]
+ })
+ const writable = await handle.createWritable()
+ await writable.write(config)
+ await writable.close()
+ } else {
+ const blob = new Blob([config], { type: 'text/plain' })
+ const url = URL.createObjectURL(blob)
+ const a = document.createElement('a')
+ a.href = url
+ a.download = 'bitcoin.conf'
+ a.click()
+ URL.revokeObjectURL(url)
+ }
+ } catch (err) {
+ if (err.name !== 'AbortError') console.error('Save failed:', err)
+ }
+ }, [config])
+
+ const handleCopyShareLink = useCallback(async () => {
+ const fullUrl = typeof window !== 'undefined' ? `${window.location.origin}${shareUrl}` : shareUrl
+ try {
+ await navigator.clipboard.writeText(fullUrl)
+ toaster.success('Link copied to clipboard')
+ } catch (err) {
+ toaster.danger('Failed to copy link')
+ }
+ }, [shareUrl, toaster])
+
+ return (
+
+
+
+
Bitcoin Config Generator
+
+ Generate a bitcoin.conf from options below. Only options that differ from defaults are included in the config.
+
+
+
+ Config file location for:
+ {
+ const newOS = e.target.value
+ setSelectedOS(newOS)
+ setOpts(prev => {
+ let changed = false
+ const next = { ...prev }
+ for (const key of OS_PATH_OPTION_KEYS) {
+ if (isPathDefaultValue(key, prev[key])) {
+ const pathDefault = getDefaultPathForOption(key, newOS)
+ if (prev[key] !== pathDefault) {
+ next[key] = pathDefault
+ changed = true
+ }
+ }
+ }
+ return changed ? next : prev
+ })
+ }}
+ size='sm'
+ className='mb-2'
+ >
+ {OS_OPTIONS.map(o => (
+
+ ))}
+
+
+ Save to:
+
+
+
+
+
+
+
+
+
+ Load preset
+ {
+ const k = e.target.value
+ if (PRESET_KEYS.includes(k)) applyPreset(k)
+ }}
+ >
+ {Object.entries(PRESETS).map(([k, v]) => (
+
+ ))}
+
+ Start from a predefined config, then tweak as needed.
+
+
+
+ {CATEGORIES.map(cat => (
+
+ {cat.title}
+
+ {cat.keys.map(key => (
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+
+ Generated bitcoin.conf
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/pages/bitcoin-config-generator/options-data.js b/pages/bitcoin-config-generator/options-data.js
new file mode 100644
index 000000000..1d0704ab7
--- /dev/null
+++ b/pages/bitcoin-config-generator/options-data.js
@@ -0,0 +1,427 @@
+/**
+ * Each option: { option (config key), label, description, widget, default }.
+ */
+
+// Select choices from "Select options reference" in OPTIONS_BY_CATEGORY.md
+export const SELECT_OPTIONS = {
+ blockfilterindex: [
+ { value: '', label: 'Disable' },
+ { value: 'basic', label: 'Basic filters' },
+ { value: '1', label: 'Enable all' }
+ ],
+ chain: [
+ { value: 'main', label: 'Mainnet' },
+ { value: 'test', label: 'Testnet (testnet3)' },
+ { value: 'testnet4', label: 'Testnet 4' },
+ { value: 'signet', label: 'Signet' },
+ { value: 'regtest', label: 'Regtest' }
+ ],
+ onlynet: [
+ { value: '', label: 'Use every available network' },
+ { value: 'ipv4', label: 'IPv4 only' },
+ { value: 'ipv6', label: 'IPv6 only' },
+ { value: 'onion', label: 'Tor only' },
+ { value: 'i2p', label: 'I2P only' },
+ { value: 'cjdns', label: 'CJDNS only' }
+ ],
+ test: [
+ { value: '', label: 'None' },
+ { value: 'addrman', label: 'Addrman' },
+ { value: 'bip94', label: 'Bip94' },
+ { value: 'reindex_after_failure_noninteractive_yes', label: 'Reindex after failure (non-interactive yes)' }
+ ],
+ debug: [
+ { value: '', label: 'Disable' },
+ { value: 'all', label: 'Enable All' },
+ { value: 'addrman', label: 'Address Manager' },
+ { value: 'alerts', label: 'Alerts' },
+ { value: 'bench', label: 'Benchmarking' },
+ { value: 'compact', label: 'Compact Blocks' },
+ { value: 'coindb', label: 'CoinDB' },
+ { value: 'db', label: 'Database' },
+ { value: 'http', label: 'HTTP' },
+ { value: 'leveldb', label: 'LevelDB' },
+ { value: 'libevent', label: 'LibEvent' },
+ { value: 'lock', label: 'Locking' },
+ { value: 'mempool', label: 'Mempool' },
+ { value: 'mempoolrej', label: 'Mempool Rejection' },
+ { value: 'net', label: 'Networking' },
+ { value: 'proxy', label: 'Proxy' },
+ { value: 'prune', label: 'Pruning' },
+ { value: 'rand', label: 'Random' },
+ { value: 'reindex', label: 'Reindex' },
+ { value: 'rpc', label: 'RPC' },
+ { value: 'selectcoins', label: 'Coin Selection' },
+ { value: 'tor', label: 'Tor' },
+ { value: 'zmq', label: 'ZeroMQ' }
+ ],
+ debugexclude: [
+ { value: '', label: 'No Exclusions' },
+ { value: 'all', label: 'Enable All' },
+ { value: 'addrman', label: 'Address Manager' },
+ { value: 'alerts', label: 'Alerts' },
+ { value: 'bench', label: 'Benchmarking' },
+ { value: 'compact', label: 'Compact Blocks' },
+ { value: 'coindb', label: 'CoinDB' },
+ { value: 'db', label: 'Database' },
+ { value: 'http', label: 'HTTP' },
+ { value: 'leveldb', label: 'LevelDB' },
+ { value: 'libevent', label: 'LibEvent' },
+ { value: 'lock', label: 'Locking' },
+ { value: 'mempool', label: 'Mempool' },
+ { value: 'mempoolrej', label: 'Mempool Rejection' },
+ { value: 'net', label: 'Networking' },
+ { value: 'proxy', label: 'Proxy' },
+ { value: 'prune', label: 'Pruning' },
+ { value: 'rand', label: 'Random' },
+ { value: 'reindex', label: 'Reindex' },
+ { value: 'rpc', label: 'RPC' },
+ { value: 'selectcoins', label: 'Coin Selection' },
+ { value: 'tor', label: 'Tor' },
+ { value: 'zmq', label: 'ZeroMQ' }
+ ],
+ deprecatedrpc: [
+ { value: '', label: 'None' },
+ { value: 'create_bdb', label: 'create_bdb' },
+ { value: 'settxfee', label: 'settxfee' },
+ { value: 'warnings', label: 'warnings' }
+ ],
+ rpccookieperms: [
+ { value: 'owner', label: 'owner' },
+ { value: 'group', label: 'group' },
+ { value: 'all', label: 'all' }
+ ],
+ addresstype: [
+ { value: 'legacy', label: 'Legacy' },
+ { value: 'p2sh-segwit', label: 'p2sh-segwit' },
+ { value: 'bech32', label: 'bech32' },
+ { value: 'bech32m', label: 'bech32m' }
+ ],
+ changetype: [
+ { value: 'legacy', label: 'Legacy' },
+ { value: 'p2sh-segwit', label: 'p2sh-segwit' },
+ { value: 'bech32', label: 'bech32' },
+ { value: 'bech32m', label: 'bech32m' }
+ ]
+}
+
+// OS-dependent file/path defaults from OPTIONS_BY_CATEGORY.md (Windows / Mac / Linux)
+const PATH_DEFAULTS_BY_OPTION = {
+ blocksdir: {
+ windows: '%APPDATA%\\Bitcoin\\blocks',
+ mac: '~/Library/Application Support/Bitcoin/blocks',
+ linux: '~/.bitcoin/blocks'
+ },
+ datadir: {
+ windows: '%APPDATA%\\Bitcoin',
+ mac: '~/Library/Application Support/Bitcoin',
+ linux: '~/.bitcoin'
+ },
+ debuglogfile: {
+ windows: '%APPDATA%\\Bitcoin\\debug.log',
+ mac: '~/Library/Application Support/Bitcoin/debug.log',
+ linux: '~/.bitcoin/debug.log'
+ },
+ asmap: {
+ windows: '%APPDATA%\\Bitcoin\\ip_asn.map',
+ mac: '~/Library/Application Support/Bitcoin/ip_asn.map',
+ linux: '~/.bitcoin/ip_asn.map'
+ },
+ walletdir: {
+ windows: '%APPDATA%\\Bitcoin\\wallets',
+ mac: '~/Library/Application Support/Bitcoin/wallets',
+ linux: '~/.bitcoin/wallets'
+ }
+}
+
+export const OS_PATH_OPTION_KEYS = Object.keys(PATH_DEFAULTS_BY_OPTION)
+
+/** Return the default path for an option for the given OS (windows/mac/linux), or '' if unknown. */
+export function getDefaultPathForOption (optionKey, os) {
+ const byOs = PATH_DEFAULTS_BY_OPTION[optionKey]
+ if (!byOs || !os) return ''
+ return byOs[os] ?? ''
+}
+
+/** True if value is empty or equals one of the OS default paths for this option. */
+export function isPathDefaultValue (optionKey, value) {
+ if (value === '' || value == null || String(value).trim() === '') return true
+ const byOs = PATH_DEFAULTS_BY_OPTION[optionKey]
+ if (!byOs) return false
+ const v = String(value).trim()
+ return Object.values(byOs).some(p => p === v)
+}
+
+// Normalize default from MD for comparison (empty = omit from conf)
+function def (val) {
+ if (val === undefined || val === null) return ''
+ const s = String(val).trim()
+ if (s === '' || s === '""' || s === '—' || s.toLowerCase() === 'any' || /^varies|^Windows:|^Mac:|^Linux:/i.test(s)) return ''
+ return s
+}
+
+// Options by category; order and descriptions follow Bitcoin Core src/init.cpp (OptionsCategory::OPTIONS)
+const OPTIONS_OPTIONS = [
+ { option: 'alertnotify', label: 'Alert Notification', description: 'Execute command when an alert is raised (%s in cmd is replaced by message).', widget: 'text', default: def('') },
+ { option: 'assumevalid', label: 'Assume Valid', description: 'If this block is in the chain assume that it and its ancestors are valid; 0 to verify all. Bitcoin Core uses chain-specific defaults.', widget: 'text', default: def('') },
+ { option: 'blocksdir', label: 'Blocks Directory', description: 'Specify directory to hold blocks subdirectory for *.dat files.', widget: 'text', default: '' },
+ { option: 'blocksxor', label: 'Blocks XOR', description: 'Whether an XOR-key applies to blocksdir *.dat files (0 or 1).', widget: 'switch', default: '1' },
+ { option: 'blocknotify', label: 'Block Notification', description: 'Execute command when the best block changes (%s in cmd is replaced by block hash).', widget: 'text', default: def('') },
+ { option: 'blockreconstructionextratxn', label: 'Block Reconstruction Extra Transactions', description: 'Extra transactions to keep in memory for compact block reconstructions.', widget: 'number', default: '100', min: 0 },
+ { option: 'blocksonly', label: 'Blocks Only', description: 'Reject transactions from network peers (broadcast/rebroadcast disabled unless peer has forcerelay). RPC transactions not affected.', widget: 'switch', default: '0' },
+ { option: 'coinstatsindex', label: 'Coin Stats Index', description: 'Maintain coinstats index used by the gettxoutsetinfo RPC.', widget: 'switch', default: '0' },
+ { option: 'datadir', label: 'Data Directory', description: 'Specify data directory.', widget: 'text', default: '' },
+ { option: 'dbbatchsize', label: 'DB Batch Size', description: 'Maximum database write batch size in bytes.', widget: 'number', default: '33554432', min: 0 },
+ { option: 'dbcache', label: 'DB Cache', description: 'Maximum database cache size in MiB (minimum 4). Unused mempool memory is shared with this cache.', widget: 'number', default: '450', min: 4 },
+ { option: 'includeconf', label: 'Include Config', description: 'Additional configuration file, relative to datadir (only from config file, not command line).', widget: 'text', default: def('') },
+ { option: 'allowignoredconf', label: 'Allow Ignored Config', description: 'Treat an unused bitcoin.conf in the datadir as a warning, not an error.', widget: 'switch', default: '0' },
+ { option: 'loadblock', label: 'Load Block', description: 'Imports blocks from external file on startup. Can be set multiple times.', widget: 'textarea', default: def('') },
+ { option: 'maxmempool', label: 'Max Mempool', description: 'Keep the transaction memory pool below N megabytes.', widget: 'number', default: '300', min: 0 },
+ { option: 'mempoolexpiry', label: 'Mempool Expiry', description: 'Do not keep transactions in the mempool longer than N hours.', widget: 'number', default: '336', min: 0 },
+ { option: 'minimumchainwork', label: 'Minimum Chain Work', description: 'Minimum work assumed to exist on a valid chain (hex). Bitcoin Core uses chain-specific defaults; this generator uses mainnet default.', widget: 'text', default: '0x000000000000000000000000000000000000000000f91c579d57cad4bc5278cc' },
+ { option: 'par', label: 'Script Verification Threads', description: 'Number of script verification threads (0=auto, <0=leave that many cores free, positive up to 256).', widget: 'number', default: '0', min: -256, max: 256 },
+ { option: 'persistmempool', label: 'Persist Mempool', description: 'Save the mempool on shutdown and load on restart.', widget: 'switch', default: '1' },
+ { option: 'persistmempoolv1', label: 'Persist Mempool Legacy Format', description: 'Write mempool.dat in legacy (v1) or current (v2) format. Temporary option.', widget: 'switch', default: '0' },
+ { option: 'pid', label: 'PID File', description: 'Specify pid file. Relative paths prefixed by net-specific datadir.', widget: 'text', default: 'bitcoind.pid' },
+ { option: 'prune', label: 'Prune', description: 'Reduce storage by pruning old blocks. Valid: 0=disable, 1=manual via RPC, ≥550=auto prune to target MiB. Values 2–549 are invalid in Bitcoin Core. Incompatible with txindex.', widget: 'number', default: '0', min: 0, max: 1000000 },
+ { option: 'reindex', label: 'Reindex', description: 'Wipe chain state and block index and rebuild from blk*.dat. Also wipes optional indexes.', widget: 'switch', default: '0' },
+ { option: 'reindex-chainstate', label: 'Reindex Chainstate', description: 'Wipe chain state and rebuild from blk*.dat on disk.', widget: 'switch', default: '0' },
+ { option: 'settings', label: 'Settings File', description: 'Path to dynamic settings data file. Written at runtime; use bitcoin.conf for custom settings.', widget: 'text', default: 'settings.json' },
+ { option: 'startupnotify', label: 'Startup Notify', description: 'Execute command on startup.', widget: 'text', default: def('') },
+ { option: 'shutdownnotify', label: 'Shutdown Notify', description: 'Execute command immediately before shutdown. Do not delay.', widget: 'text', default: def('') },
+ { option: 'txindex', label: 'Transaction Index', description: 'Maintain a full transaction index for the getrawtransaction RPC.', widget: 'switch', default: '0' },
+ { option: 'txospenderindex', label: 'TxOut Spender Index', description: 'Maintain a transaction output spender index for the gettxspendingprevout RPC.', widget: 'switch', default: '0' },
+ { option: 'blockfilterindex', label: 'Block Filter Index', description: 'Maintain an index of compact filters by block (0, basic, or 1=all types).', widget: 'select', default: '' },
+ { option: 'daemon', label: 'Daemon', description: 'Run in the background as a daemon and accept commands.', widget: 'switch', default: '0' },
+ { option: 'daemonwait', label: 'Daemon Wait', description: 'Wait for initialization to finish before exiting. Implies -daemon.', widget: 'switch', default: '0' }
+]
+
+// Debugging/testing options; order follows init.cpp OptionsCategory::DEBUG_TEST (+ logging from AddLoggingArgs)
+const DEBUG_OPTIONS = [
+ { option: 'checkblocks', label: 'Check Blocks', description: 'How many blocks to check at startup (0 = all).', widget: 'number', default: '6', min: 0 },
+ { option: 'checklevel', label: 'Check Level', description: 'How thorough the block verification of -checkblocks is (0–4).', widget: 'number', default: '3', min: 0, max: 4 },
+ { option: 'checkblockindex', label: 'Check Block Index', description: 'Consistency check for block tree, chainstate, etc. every N operations. 0 to disable.', widget: 'number', default: '0', min: 0 },
+ { option: 'checkaddrman', label: 'Check Addrman', description: 'Run addrman consistency checks every N operations. 0 to disable.', widget: 'number', default: '0', min: 0 },
+ { option: 'checkmempool', label: 'Check Mempool', description: 'Run mempool consistency checks every N transactions. 0 to disable.', widget: 'number', default: '0', min: 0 },
+ { option: 'deprecatedrpc', label: 'Deprecated RPC', description: 'Allow deprecated RPC method(s) to be used. Enable one or more.', widget: 'multi_switch', default: '', multiSwitchOptions: [{ value: 'create_bdb', label: 'create_bdb' }, { value: 'settxfee', label: 'settxfee' }, { value: 'warnings', label: 'warnings' }] },
+ { option: 'stopafterblockimport', label: 'Stop After Block Import', description: 'Stop running after importing blocks from disk.', widget: 'switch', default: '0' },
+ { option: 'stopatheight', label: 'Stop At Height', description: 'Stop running after reaching the given height in the main chain. Blocks after target may be processed during shutdown.', widget: 'number', default: '0', min: 0 },
+ { option: 'test', label: 'Test', description: 'Pass a test-only option (e.g. addrman, bip94). Only applies when chain=regtest.', widget: 'select', default: '' },
+ { option: 'limitclustercount', label: 'Limit Cluster Count', description: 'Do not accept tx into mempool connected to ≥ N other unconfirmed tx (max 64).', widget: 'number', default: '64', min: 0, max: 64 },
+ { option: 'limitclustersize', label: 'Limit Cluster Size', description: 'Do not accept tx whose virtual size with connected in-mempool tx exceeds N kB.', widget: 'number', default: '101', min: 0 },
+ { option: 'capturemessages', label: 'Capture Messages', description: 'Capture all P2P messages to disk.', widget: 'switch', default: '0' },
+ { option: 'mocktime', label: 'Mock Time', description: 'Replace actual time with Unix epoch time (default: 0).', widget: 'number', default: '0', min: 0 },
+ { option: 'maxsigcachesize', label: 'Max Sig Cache Size', description: 'Limit sum of signature cache and script execution cache sizes to N MiB.', widget: 'number', default: '32', min: 0 },
+ { option: 'maxtipage', label: 'Max Tip Age', description: 'Maximum tip age in seconds to consider node in initial block download.', widget: 'number', default: '86400', min: 0 },
+ { option: 'printpriority', label: 'Print Priority', description: 'Log transaction fee rate in BTC/kvB when mining blocks.', widget: 'switch', default: '0' },
+ { option: 'uacomment', label: 'User-Agent Comment', description: 'Append comment to the user agent string.', widget: 'text', default: def('') },
+ { option: 'acceptstalefeeestimates', label: 'Accept Stale Fee Estimates', description: 'Read fee estimates even if stale (regtest only). Stale = over 60 hours old.', widget: 'switch', default: '0' },
+ { option: 'dbcrashratio', label: 'DB Crash Ratio', description: 'Randomly crash while writing at given rate 0–1 (testing). Hidden in bitcoind -help.', widget: 'number_float_8', default: '0', min: 0, max: 1 },
+ { option: 'fastprune', label: 'Fast Prune', description: 'Use smaller block files and lower minimum prune height for testing.', widget: 'switch', default: '0' },
+ { option: 'testactivationheight', label: 'Test Activation Height', description: "Set activation height of 'name' (segwit, bip34, dersig, cltv, csv). Regtest-only. Format: name@height.", widget: 'text', default: def('') },
+ { option: 'scriptverifyflag', label: 'Script Verify Flag', description: 'Integer script verification flags to enable.', widget: 'text', default: def('') },
+ { option: 'maxtxfee', label: 'Max Tx Fee', description: 'Maximum total fees (BTC) in a single wallet or raw transaction. Too low may abort large tx.', widget: 'number_float_8', default: '0.10', min: 0 },
+ { option: 'debug', label: 'Debug', description: 'Output debug and trace logging. Category optional; 1 or "all" for all.', widget: 'select', default: '' },
+ { option: 'debugexclude', label: 'Debug Exclude', description: 'Exclude debug logging for category. Takes priority over -debug.', widget: 'select', default: '' },
+ { option: 'debuglogfile', label: 'Debug Log File', description: 'Location of debug log. Relative paths prefixed by net-specific datadir. -nodebuglogfile disables file.', widget: 'text', default: '' },
+ { option: 'logips', label: 'Log IPs', description: 'Include IP addresses in log output.', widget: 'switch', default: '0' },
+ { option: 'loglevel', label: 'Log Level', description: 'Global or per-category severity (info, debug, trace). Category:level overrides global.', widget: 'text', default: def('') },
+ { option: 'loglevelalways', label: 'Log Level Always', description: 'Always prepend category and level to each log message.', widget: 'switch', default: '0' },
+ { option: 'logratelimit', label: 'Log Rate Limit', description: 'Apply rate limiting to unconditional logging to mitigate disk-filling.', widget: 'switch', default: '1' },
+ { option: 'logsourcelocations', label: 'Log Source Locations', description: 'Prepend source location (file, line, function) to debug output.', widget: 'switch', default: '0' },
+ { option: 'logthreadnames', label: 'Log Thread Names', description: 'Prepend thread name to debug output.', widget: 'switch', default: '0' },
+ { option: 'logtimestamps', label: 'Log Timestamps', description: 'Prepend timestamp to debug output.', widget: 'switch', default: '1' },
+ { option: 'logtimemicros', label: 'Log Time Micros', description: 'Microsecond precision in debug timestamps.', widget: 'switch', default: '0' },
+ { option: 'printtoconsole', label: 'Print to Console', description: 'Send trace/debug to console (default when no -daemon). Use -nodebuglogfile to disable file.', widget: 'switch', default: '0' },
+ { option: 'shrinkdebugfile', label: 'Shrink Debug File', description: 'Shrink debug.log on client startup (default when no -debug).', widget: 'switch', default: '1' }
+]
+
+// Chain selection; order follows chainparamsbase.cpp SetupChainParamsBaseOptions (OptionsCategory::CHAINPARAMS)
+const CHAIN_OPTIONS = [
+ { option: 'chain', label: 'Chain', description: 'Use the chain (main, test, testnet4, signet, regtest).', widget: 'select', default: 'main' },
+ { option: 'regtest', label: 'Regtest', description: 'Regression test mode; blocks can be solved instantly. Equivalent to -chain=regtest.', widget: 'switch', default: '0' },
+ { option: 'testnet4', label: 'Testnet 4', description: 'Use the testnet4 chain. Equivalent to -chain=testnet4.', widget: 'switch', default: '0' },
+ { option: 'vbparams', label: 'Version Bits Params', description: 'Version bits deployment start/end and min_activation_height. Regtest-only. Format: deployment:start:end[:min_activation_height].', widget: 'text', default: def('') },
+ { option: 'signet', label: 'Signet', description: 'Use the signet chain. Equivalent to -chain=signet. Network defined by -signetchallenge.', widget: 'switch', default: '0' },
+ { option: 'signetchallenge', label: 'Signet Challenge', description: 'Blocks must satisfy the given script (signet only). Defaults to global default signet test network challenge.', widget: 'text', default: def('') },
+ { option: 'signetseednode', label: 'Signet Seed Node', description: 'Seed node for signet, hostname[:port]. Can be set multiple times. Defaults to global default signet seed node(s).', widget: 'textarea', default: def('') }
+]
+
+// Block creation; order follows init.cpp OptionsCategory::BLOCK_CREATION
+const BLOCK_CREATION_OPTIONS = [
+ { option: 'blockmaxweight', label: 'Block Max Weight', description: 'Set maximum BIP141 block weight (max 4000000).', widget: 'number', default: '4000000', min: 0, max: 4000000 },
+ { option: 'blockreservedweight', label: 'Block Reserved Weight', description: 'Reserve space for block header plus largest coinbase. Min 2000, max 4000000. Affects mining RPC clients only.', widget: 'number', default: '8000', min: 2000, max: 4000000 },
+ { option: 'blockmintxfee', label: 'Block Min Tx Fee', description: 'Lowest fee rate (BTC/kvB) for tx to be included in block creation.', widget: 'number_float_8', default: '0.00001', min: 0 },
+ { option: 'blockversion', label: 'Block Version', description: 'Override block version to test forking scenarios.', widget: 'text', default: def('') }
+]
+
+// Connection options; order follows init.cpp OptionsCategory::CONNECTION
+const CONNECTION_OPTIONS = [
+ { option: 'addnode', label: 'Add Node', description: 'Add a node to connect to and attempt to keep the connection open. Can be set multiple times (limit 8).', widget: 'textarea', default: def('') },
+ { option: 'asmap', label: 'ASN Mapping', description: 'ASN mapping used for bucketing peers. Relative paths prefixed by net-specific datadir.', widget: 'text', default: '' },
+ { option: 'bantime', label: 'Ban Time', description: 'Default duration in seconds for manually configured bans.', widget: 'number', default: '86400', min: 0 },
+ { option: 'bind', label: 'Bind', description: 'Bind to given address and always listen. Use [host]:port for IPv6. Append =onion for Tor.', widget: 'text', default: '0.0.0.0' },
+ { option: 'cjdnsreachable', label: 'CJDNS Reachable', description: 'If set, this host is configured for CJDNS (fc00::/8).', widget: 'switch', default: '0' },
+ { option: 'connect', label: 'Connect', description: 'Connect only to the specified node(s); -noconnect disables automatic connections. Can be set multiple times.', widget: 'textarea', default: def('') },
+ { option: 'discover', label: 'Discover', description: 'Discover own IP addresses (when listening and no -externalip or -proxy).', widget: 'switch', default: '1' },
+ { option: 'dns', label: 'DNS Lookups', description: 'Allow DNS lookups for -addnode, -seednode and -connect.', widget: 'switch', default: '1' },
+ { option: 'dnsseed', label: 'DNS Seed', description: 'Query for peer addresses via DNS lookup when low on addresses.', widget: 'switch', default: '1' },
+ { option: 'externalip', label: 'External IP', description: 'Specify your own public address.', widget: 'text', default: def('') },
+ { option: 'fixedseeds', label: 'Fixed Seeds', description: 'Allow fixed seeds if DNS seeds don\'t provide peers.', widget: 'switch', default: '1' },
+ { option: 'forcednsseed', label: 'Force DNS Seed', description: 'Always query for peer addresses via DNS lookup.', widget: 'switch', default: '0' },
+ { option: 'listen', label: 'Listen', description: 'Accept connections from outside.', widget: 'switch', default: '1' },
+ { option: 'listenonion', label: 'Listen Onion', description: 'Automatically create Tor onion service.', widget: 'switch', default: '1' },
+ { option: 'maxconnections', label: 'Max Connections', description: 'Maintain at most N automatic connections to peers. Does not apply to -addnode or private broadcast.', widget: 'number', default: '125', min: 0 },
+ { option: 'maxreceivebuffer', label: 'Max Receive Buffer', description: 'Maximum per-connection receive buffer (×1000 bytes).', widget: 'number', default: '5000', min: 0 },
+ { option: 'maxsendbuffer', label: 'Max Send Buffer', description: 'Maximum per-connection send buffer (×1000 bytes).', widget: 'number', default: '1000', min: 0 },
+ { option: 'maxuploadtarget', label: 'Max Upload Target', description: 'Keep outbound traffic under target per 24h (MiB). 0 = no limit. This generator uses MiB only; Bitcoin Core also accepts suffixes k/K/m/M/g/G/t/T in config.', widget: 'number', default: '0', min: 0 },
+ { option: 'onion', label: 'Onion Proxy', description: 'Separate SOCKS5 proxy for Tor onion services; -noonion to disable. Defaults to -proxy.', widget: 'text', default: def('') },
+ { option: 'i2psam', label: 'I2P SAM Proxy', description: 'I2P SAM proxy to reach I2P peers and accept I2P connections.', widget: 'text', default: def('') },
+ { option: 'i2pacceptincoming', label: 'I2P Accept Incoming', description: 'Accept inbound I2P connections. Ignored if -i2psam not set.', widget: 'switch', default: '1' },
+ { option: 'onlynet', label: 'Only Net', description: 'Make automatic outbound connections only to network (ipv4, ipv6, onion, i2p, cjdns). This generator allows one network; Bitcoin Core allows multiple onlynet lines.', widget: 'select', default: '' },
+ { option: 'v2transport', label: 'V2 Transport', description: 'Support v2 transport (BIP324).', widget: 'switch', default: '1' },
+ { option: 'peerbloomfilters', label: 'Peer Bloom Filters', description: 'Support filtering of blocks and transactions with bloom filters.', widget: 'switch', default: '0' },
+ { option: 'peerblockfilters', label: 'Peer Block Filters', description: 'Serve compact block filters to peers per BIP 157.', widget: 'switch', default: '0' },
+ { option: 'txreconciliation', label: 'Tx Reconciliation', description: 'Enable transaction reconciliations per BIP 330.', widget: 'switch', default: '0' },
+ { option: 'port', label: 'Port', description: 'Listen for connections on port. Core default is chain-specific (e.g. 8333 mainnet). 0 = use Core default. Not relevant for I2P. Onion port defaults to port+1.', widget: 'number', default: '0', min: 0 },
+ { option: 'proxy', label: 'Proxy', description: 'Connect through SOCKS5 proxy; -noproxy to disable. May end in =network (ipv4, ipv6, tor, cjdns).', widget: 'text', default: def('') },
+ { option: 'proxyrandomize', label: 'Proxy Randomize', description: 'Randomize credentials for every proxy connection (Tor stream isolation).', widget: 'switch', default: '1' },
+ { option: 'seednode', label: 'Seed Node', description: 'Connect to a node to retrieve peer addresses and disconnect. Can be set multiple times. Tried before dnsseeds.', widget: 'textarea', default: def('') },
+ { option: 'networkactive', label: 'Network Active', description: 'Enable all P2P network activity. Can be changed by setnetworkactive RPC.', widget: 'switch', default: '1' },
+ { option: 'timeout', label: 'Timeout', description: 'Socket connection timeout in milliseconds before dropping an attempt (minimum 1).', widget: 'number', default: '5000', min: 1 },
+ { option: 'peertimeout', label: 'Peer Timeout', description: 'Seconds of inactivity before considering disconnection (after connecting). Must be ≥ 1.', widget: 'number', default: '60', min: 1 },
+ { option: 'torcontrol', label: 'Tor Control', description: 'Tor control host and port if onion listening enabled. Default port 9051 if omitted.', widget: 'text', default: '127.0.0.1:9051' },
+ { option: 'torpassword', label: 'Tor Password', description: 'Tor control port password.', widget: 'password', default: def('') },
+ { option: 'natpmp', label: 'NAT-PMP', description: 'Use PCP or NAT-PMP to map the listening port.', widget: 'switch', default: '0' },
+ { option: 'whitebind', label: 'Whitebind', description: 'Bind to address and add permission flags for peers connecting. Permissions: bloomfilter, noban, forcerelay, relay, mempool, download, addr. Can be set multiple times.', widget: 'text', default: def('') },
+ { option: 'whitelist', label: 'Whitelist', description: 'Add permission flags for peers at given IP or CIDR. Same permissions as -whitebind. Flags "in" and "out" control direction. Can be set multiple times.', widget: 'textarea', default: def('') }
+]
+
+// Node relay; order follows init.cpp OptionsCategory::NODE_RELAY
+const RELAY_OPTIONS = [
+ { option: 'acceptnonstdtxn', label: 'Accept Non-Standard Tx', description: 'Relay and mine "non-standard" transactions (test networks only).', widget: 'switch', default: '0' },
+ { option: 'incrementalrelayfee', label: 'Incremental Relay Fee', description: 'Fee rate (BTC/kvB) for cost of relay; mempool limiting and replacement policy.', widget: 'number_float_8', default: '0.00001', min: 0 },
+ { option: 'dustrelayfee', label: 'Dust Relay Fee', description: 'Fee rate (BTC/kvB) used to define dust (output that costs more than its value in fees at this rate to spend).', widget: 'number_float_8', default: '0.00001', min: 0 },
+ { option: 'bytespersigop', label: 'Bytes Per Sigop', description: 'Equivalent bytes per sigop in transactions for relay and mining.', widget: 'number', default: '20', min: 0 },
+ { option: 'datacarrier', label: 'Data Carrier', description: 'Relay and mine data carrier (OP_RETURN) transactions.', widget: 'switch', default: '1' },
+ { option: 'datacarriersize', label: 'Data Carrier Size', description: 'Max aggregate size of data-carrying scriptPubKeys we relay and mine. Multiple outputs allowed.', widget: 'number', default: '100000', min: 0 },
+ { option: 'permitbaremultisig', label: 'Permit Bare Multisig', description: 'Relay transactions creating non-P2SH multisig outputs.', widget: 'switch', default: '1' },
+ { option: 'minrelaytxfee', label: 'Min Relay Tx Fee', description: 'Fees (BTC/kvB) smaller than this are zero fee for relaying, mining and transaction creation.', widget: 'number_float_8', default: '0.00001', min: 0 },
+ { option: 'privatebroadcast', label: 'Private Broadcast', description: 'Broadcast sendrawtransaction RPC tx via short-lived Tor/I2P connections without putting in mempool. Wallet tx not affected.', widget: 'switch', default: '0' },
+ { option: 'whitelistforcerelay', label: 'Whitelist Force Relay', description: "Add 'forcerelay' to whitelisted peers with default permissions; relay tx even if already in mempool.", widget: 'switch', default: '0' },
+ { option: 'whitelistrelay', label: 'Whitelist Relay', description: "Add 'relay' to whitelisted peers; accept relayed tx even when not relaying.", widget: 'switch', default: '1' }
+]
+
+// RPC server; order follows init.cpp OptionsCategory::RPC (+ IPC)
+const RPC_OPTIONS = [
+ { option: 'rest', label: 'REST', description: 'Accept public REST requests.', widget: 'switch', default: '0' },
+ { option: 'rpcallowip', label: 'RPC Allow IP', description: 'Allow JSON-RPC from specified source (IP, netmask, CIDR, 0.0.0.0/0, ::/0). Can be set multiple times.', widget: 'textarea', default: def('') },
+ { option: 'rpcauth', label: 'RPC Auth', description: 'Username and HMAC-SHA-256 hashed password for JSON-RPC. Format: username:salt$hash. Can be set multiple times. Client uses rpcuser/rpcpassword.', widget: 'text', default: def('') },
+ { option: 'rpcbind', label: 'RPC Bind', description: 'Bind to address to listen for JSON-RPC. Ignored unless -rpcallowip passed. Port optional, overrides -rpcport. Can be set multiple times.', widget: 'textarea', default: def('') },
+ { option: 'rpcdoccheck', label: 'RPC Doc Check', description: 'Throw non-fatal error at runtime if RPC documentation is incorrect.', widget: 'switch', default: '0' },
+ { option: 'rpccookiefile', label: 'RPC Cookie File', description: 'Location of auth cookie. Relative paths prefixed by net-specific datadir.', widget: 'text', default: '.cookie' },
+ { option: 'rpccookieperms', label: 'RPC Cookie Perms', description: 'Make cookie readable by owner, group, or all.', widget: 'select', default: 'owner' },
+ { option: 'rpcpassword', label: 'RPC Password', description: 'Password for JSON-RPC (legacy; prefer rpcauth).', widget: 'password', default: def('') },
+ { option: 'rpcport', label: 'RPC Port', description: 'Listen for JSON-RPC on port. Varies by network.', widget: 'number', default: '8332', min: 0 },
+ { option: 'rpcservertimeout', label: 'RPC Server Timeout', description: 'Timeout during HTTP requests (seconds).', widget: 'number', default: '30', min: 0 },
+ { option: 'rpcthreads', label: 'RPC Threads', description: 'Number of threads to service RPC calls.', widget: 'number', default: '16', min: 0 },
+ { option: 'rpcuser', label: 'RPC User', description: 'Username for JSON-RPC (legacy; prefer rpcauth).', widget: 'text', default: def('') },
+ { option: 'rpcwhitelist', label: 'RPC Whitelist', description: 'Filter incoming RPC calls for a user. Format: username:rpc1,rpc2,... Multiple whitelists set-intersected. See -rpcwhitelistdefault.', widget: 'text', default: def('') },
+ { option: 'rpcwhitelistdefault', label: 'RPC Whitelist Default', description: 'Default whitelist behavior. If any -rpcwhitelist: all users subject to empty-unless-specified. If 1 and none: all subject to empty.', widget: 'switch', default: '0' },
+ { option: 'rpcworkqueue', label: 'RPC Work Queue', description: 'Maximum depth of work queue to service RPC calls.', widget: 'number', default: '64', min: 0 },
+ { option: 'server', label: 'Server', description: 'Accept command line and JSON-RPC commands.', widget: 'switch', default: '0' },
+ { option: 'ipcbind', label: 'IPC Bind', description: 'Bind to Unix socket for incoming connections. "unix" = default path, "unix:/path" = custom. Can be set multiple times.', widget: 'text', default: def('') }
+]
+
+const WALLET_OPTIONS = [
+ { option: 'addresstype', label: 'Address Type', description: 'Address type for receiving (legacy, p2sh-segwit, bech32, bech32m).', widget: 'select', default: 'p2sh-segwit' },
+ { option: 'avoidpartialspends', label: 'Avoid Partial Spends', description: 'Group outputs by address, selecting all or none.', widget: 'switch', default: '0' },
+ { option: 'changetype', label: 'Change Address Type', description: 'Change address type (legacy, p2sh-segwit, bech32, bech32m).', widget: 'select', default: 'p2sh-segwit' },
+ { option: 'consolidatefeerate', label: 'Consolidate Fee Rate', description: 'The maximum feerate (in BTC/kvB) at which transaction building may use more inputs.', widget: 'number_float_8', default: '0.0001', min: 0 },
+ { option: 'disablewallet', label: 'Disable Wallet', description: 'Do not load the wallet and disable wallet RPC calls.', widget: 'switch', default: '0' },
+ { option: 'keypool', label: 'Key Pool Size', description: 'Set key pool size.', widget: 'number', default: '1000', min: 0 },
+ { option: 'fallbackfee', label: 'Fallback Transaction Fee', description: 'A fee rate (in BTC/kB) that will be used when fee estimation has insufficient data', widget: 'number_float_8', default: '0.0002', min: 0 },
+ { option: 'discardfee', label: 'Discard Change Fee', description: 'The fee rate (in BTC/kB) that indicates your tolerance for discarding change by adding it to the fee', widget: 'number_float_8', default: '0.0001', min: 0 },
+ { option: 'maxapsfee', label: 'Max Avoid Partial Spend Fee', description: 'Spend up to this amount in additional (absolute) fees (in BTC) if it allows the use of partial spend avoidance.', widget: 'number_float_8', default: '0.00', min: 0 },
+ { option: 'mintxfee', label: 'Min Transaction Fee', description: 'Fee rates (in BTC/kB) smaller than this are considered zero fee for transaction creation', widget: 'number_float_8', default: '0.00001', min: 0 },
+ { option: 'paytxfee', label: 'Pay Transaction Fee', description: 'Fee rate (in BTC/kB) to add to transactions you send. Not recommended to set!', widget: 'number_float_8', default: '0', min: 0 },
+ { option: 'signer', label: 'Signer Command', description: 'External signing tool, see doc/external-signer.md', widget: 'text', default: def('') },
+ { option: 'spendzeroconfchange', label: 'Spend Unconfirmed Change', description: 'Spend unconfirmed change when sending transactions.', widget: 'switch', default: '1' },
+ { option: 'rootcertificates', label: 'Root Certificate file', description: 'Specify a custom root certificate to trust for payment requests.', widget: 'text', default: '-system-' },
+ { option: 'txconfirmtarget', label: 'Transaction Fee Confirmation Target', description: 'If paytxfee is not set, include enough fee so that transactions should confirm within blocks', widget: 'number', default: '6', min: 0 },
+ { option: 'unsafesqlitesync', label: 'Unsafe SQL Lite Sync', description: 'Disable waiting for the database to sync to disk. This is unsafe.', widget: 'switch', default: '0' },
+ { option: 'walletrbf', label: 'Enable Replace By Fee Transactions', description: 'Send transactions with full-RBF opt-in enabled.', widget: 'switch', default: '1' },
+ { option: 'wallet', label: 'Wallet Path', description: 'Specify wallet database path. Can be specified multiple times.', widget: 'text', default: def('') },
+ { option: 'walletdir', label: 'Wallet Data Storage Location', description: 'Specify a non-default location to store wallet data.', widget: 'text', default: '' },
+ { option: 'walletbroadcast', label: 'Broadcast Transactions', description: 'Broadcast transactions created by the wallet.', widget: 'switch', default: '1' },
+ { option: 'walletnotify', label: 'Wallet Notification', description: 'Execute command when a wallet transaction changes (%s in cmd is replaced by TxID)', widget: 'text', default: def('') },
+ { option: 'walletrejectlongchains', label: 'Reject Long Transaction Chains', description: 'Wallet will not create transactions that violate mempool chain limits.', widget: 'switch', default: '1' },
+ { option: 'walletcrosschain', label: 'Wallet Cross Chain', description: 'Allow reusing wallet files across chains.', widget: 'switch', default: '0' }
+]
+
+const ZMQ_OPTIONS = [
+ { option: 'zmqpubhashblock', label: 'Publish Block Hashes', description: 'Enable publishing of block hashes to ZMQ endpoint (e.g. tcp://127.0.0.1:28332).', widget: 'text', default: def('') },
+ { option: 'zmqpubhashtx', label: 'Publish Transaction Hashes', description: 'Enable publishing of transaction hashes to ZMQ endpoint (e.g. tcp://127.0.0.1:28332).', widget: 'text', default: def('') },
+ { option: 'zmqpubrawblock', label: 'Publish Raw Blocks', description: 'Enable publishing of raw block hex to ZMQ endpoint (e.g. tcp://127.0.0.1:28332).', widget: 'text', default: def('') },
+ { option: 'zmqpubrawtx', label: 'Publish Raw Transactions', description: 'Enable publishing of raw transaction hex to ZMQ endpoint (e.g. tcp://127.0.0.1:28332).', widget: 'text', default: def('') },
+ { option: 'zmqpubsequence', label: 'Publish Hash Block and Transaction Sequence', description: 'Enable publishing of hash block and tx sequence to ZMQ endpoint (e.g. tcp://127.0.0.1:28332).', widget: 'text', default: def('') },
+ { option: 'zmqpubhashblockhwm', label: 'Publish Block Hashes High Water Mark', description: 'Set publish hash block outbound message high water mark.', widget: 'number', default: '1000', min: 0 },
+ { option: 'zmqpubhashtxhwm', label: 'Publish Transaction Hashes High Water Mark', description: 'Set publish hash transaction outbound message high water mark.', widget: 'number', default: '1000', min: 0 },
+ { option: 'zmqpubrawblockhwm', label: 'Publish Raw Blocks High Water Mark', description: 'Set publish raw block outbound message high water mark.', widget: 'number', default: '1000', min: 0 },
+ { option: 'zmqpubrawtxhwm', label: 'Publish Raw Transactions High Water Mark', description: 'Set publish raw transaction outbound message high water mark.', widget: 'number', default: '1000', min: 0 },
+ { option: 'zmqpubsequencehwm', label: 'Publish Hash Sequence Message High Water Mark', description: 'Set publish hash sequence message high water mark.', widget: 'number', default: '1000', min: 0 }
+]
+
+// Category order follows Bitcoin Core init.cpp: Options → Connection → Wallet → ZMQ → Debug → Chain → Node Relay → Block Creation → RPC
+export const ALL_OPTIONS = [
+ ...OPTIONS_OPTIONS,
+ ...CONNECTION_OPTIONS,
+ ...WALLET_OPTIONS,
+ ...ZMQ_OPTIONS,
+ ...DEBUG_OPTIONS,
+ ...CHAIN_OPTIONS,
+ ...RELAY_OPTIONS,
+ ...BLOCK_CREATION_OPTIONS,
+ ...RPC_OPTIONS
+]
+
+export const OPTIONS_BY_KEY = Object.fromEntries(ALL_OPTIONS.map(o => [o.option, o]))
+
+export const CATEGORIES = [
+ { id: 'options', title: 'Options', keys: OPTIONS_OPTIONS.map(o => o.option) },
+ { id: 'connection', title: 'Connection', keys: CONNECTION_OPTIONS.map(o => o.option) },
+ { id: 'wallet', title: 'Wallet', keys: WALLET_OPTIONS.map(o => o.option) },
+ { id: 'zeromq', title: 'ZeroMQ', keys: ZMQ_OPTIONS.map(o => o.option) },
+ { id: 'debugging', title: 'Debugging & Testing', keys: DEBUG_OPTIONS.map(o => o.option) },
+ { id: 'chain', title: 'Chain Selection', keys: CHAIN_OPTIONS.map(o => o.option) },
+ { id: 'relay', title: 'Transaction Relay', keys: RELAY_OPTIONS.map(o => o.option) },
+ { id: 'blockcreation', title: 'Block Creation', keys: BLOCK_CREATION_OPTIONS.map(o => o.option) },
+ { id: 'rpc', title: 'RPC API', keys: RPC_OPTIONS.map(o => o.option) }
+]
+
+/** Parse default for initial state: empty string for "omit", normalized value otherwise */
+export function parseDefault (opt) {
+ const d = opt.default
+ if (opt.widget === 'switch') {
+ if (d === '' || d === '0' || d === 'false') return false
+ if (d === '1' || d === 'true') return true
+ return false
+ }
+ if (opt.widget === 'number' || opt.widget === 'number_float_8') {
+ if (d === '') return ''
+ const n = Number(String(d).replace(/,.*/, '').trim())
+ return isNaN(n) ? (opt.widget === 'number_float_8' ? d : '') : (opt.widget === 'number_float_8' ? String(d) : n)
+ }
+ if (opt.widget === 'select' && d === '') return ''
+ if (opt.widget === 'multi_switch') return d === undefined || d === null ? '' : (typeof d === 'string' ? d : '')
+ return d === undefined || d === null ? '' : String(d)
+}
+
+/** Build initial state from defaults */
+export function getInitialState () {
+ const state = {}
+ ALL_OPTIONS.forEach(o => {
+ state[o.option] = parseDefault(o)
+ })
+ return state
+}