Files
AtreidesAssetsFree/shared/config.js
T

871 lines
39 KiB
JavaScript
Raw Permalink Normal View History

2026-10-29 11:57:29 +01:00
// Generated, do not edit. sha256:79db5d8cc5706ad987b207d98e1c67ca389284a24121f65b199acbe48bce76ec
const GRAMS_PER_TROY_OUNCE = 31.1034768;
export const CONFIG = Object.freeze({
PRICE_SOURCES: Object.freeze({
MEMPOOL_HISTORICAL: '/api/v1/historical-price',
MEMPOOL_PRICES: '/api/v1/prices',
MEMPOOL_PUBLIC_BASE: 'https://mempool.space',
BITVAVO_BASE: 'https://api.bitvavo.com',
BITVAVO_CANDLES: '/v2/{market}/candles',
BITVAVO_TICKER: '/v2/ticker/price',
BITVAVO_ACCOUNT: '/v2/account',
COINGECKO_BASE: 'https://api.coingecko.com/api/v3',
COINGECKO_PRICE: '/simple/price',
COINGECKO_CHART: '/coins/{id}/market_chart',
COINGECKO_PING: '/ping',
GOLDAPI_BASE: 'https://www.goldapi.io',
GOLDAPI_SPOT: '/api/{metal}/{currency}',
GOLDAPI_HISTORICAL: '/api/{metal}/{currency}/{date}',
EULERPOOL_BASE: 'https://api.eulerpool.com',
EULERPOOL_QUOTES: '/api/1/{external_id}/quotes/{symbol}',
METALSDEV_BASE: 'https://api.metals.dev',
METALSDEV_SPOT: '/v1/metal/spot',
METALSDEV_TIMESERIES: '/v1/timeseries',
FRANKFURTER_BASE: 'https://api.frankfurter.dev',
FRANKFURTER_LATEST: '/v1/latest',
FRANKFURTER_SERIES: '/v1/1999-01-04..',
}),
CHART_LIBS: Object.freeze({
CHARTJS: 'vendor/chartjs/chart.umd.js',
HAMMER: 'vendor/hammerjs/hammer.min.js',
ZOOM: 'vendor/chartjs-plugin-zoom/chartjs-plugin-zoom.min.js',
}),
SCHEMA_VERSION: 128,
MAX_ASSET_DECIMALS: 9,
LOCALE_TAGS: Object.freeze({ nl: 'nl-NL', en: 'en-US' }),
INSTALLATION: Object.freeze({ SETTINGS_FILE_NAME: 'installation.json', DEFAULT_LOCALE: 'en' }),
METALS: Object.freeze({
BASE_UNIT: 'g',
BASE_DECIMALS: 7,
GRAMS_PER_TROY_OUNCE,
DISPLAY_DECIMALS: 4,
UNITS: Object.freeze({ g: 1, kg: 1000, ozt: GRAMS_PER_TROY_OUNCE }),
SPOT_DEVIATION_WARN: 0.10,
}),
DB_TABLES: Object.freeze([
'Asset', 'Account', 'Event', 'Valuation', 'Label',
'WalletSource', 'WalletAddress', 'WalletTransaction', 'ScanLog',
'ReconcileProposal', 'ImportBatch', 'PriceHistory', 'WalletTransactionEventLink',
'InterestSchedule', 'InterestPeriod', 'InterestCredit', 'Notification',
'MetalProduct',
'Provider', 'ProviderAuthenticationHmac', 'ProviderEndpoint', 'ProviderFixedRate', 'ProviderCallLimit',
'ProviderInputMap', 'ProviderOutputMap', 'ProviderCoverage', 'ApiCallMetric',
'PriceSource', 'Connection',
'DashboardProfile', 'DashboardWidget',
'AiToolCall', 'AiToolCallMessage', 'BusSignal',
'Command', 'AiProposal',
'Preference', 'Schema', 'ChangeLog', 'HygieneAcceptance', 'IgnoredPriceGap', 'AssetNote',
'Portfolio', 'PriceAbsence', 'IntegrationCall',
]),
DB_DELETABLE_TABLES: Object.freeze([
'ImportBatch', 'PriceHistory', 'ReconcileProposal', 'WalletTransactionEventLink',
'Valuation', 'WalletAddress', 'InterestCredit', 'Notification',
'PriceSource', 'ProviderCoverage', 'Preference', 'BusSignal',
'PriceAbsence',
'IntegrationCall',
]),
DB_HISTORY: Object.freeze({
TRACKED: Object.freeze([
'Account', 'Asset', 'Event', 'Valuation', 'Label', 'MetalProduct',
'InterestSchedule', 'InterestPeriod', 'InterestCredit',
'WalletSource', 'Connection',
]),
UNTRACKED: Object.freeze({
AiToolCall: 'logboek', AiToolCallMessage: 'logboek', BusSignal: 'logboek',
Command: 'logboek', ScanLog: 'logboek', ApiCallMetric: 'logboek', Notification: 'logboek',
WalletTransaction: 'scanketen', WalletAddress: 'scanketen', WalletTransactionEventLink: 'scanketen',
PriceHistory: 'volume', PriceAbsence: 'volume',
AiProposal: 'herkomst', ReconcileProposal: 'herkomst', ImportBatch: 'herkomst',
Provider: 'configuratie', ProviderAuthenticationHmac: 'configuratie',
ProviderEndpoint: 'configuratie', ProviderFixedRate: 'configuratie', ProviderCallLimit: 'configuratie',
ProviderInputMap: 'configuratie', ProviderOutputMap: 'configuratie',
ProviderCoverage: 'configuratie', PriceSource: 'configuratie',
DashboardProfile: 'weergave', DashboardWidget: 'weergave', Preference: 'weergave',
Schema: 'systeem', Portfolio: 'systeem',
ChangeLog: 'logboek',
IntegrationCall: 'logboek',
HygieneAcceptance: 'weergave',
IgnoredPriceGap: 'weergave',
AssetNote: 'weergave',
}),
}),
PEILDATUM: Object.freeze({
MONTH: 1, DAY: 1, HOUR: 0, MINUTE: 0,
TIMEZONE: 'Europe/Amsterdam',
}),
START_YEAR: Object.freeze({
AUTO: 0,
MIN: 1900,
MAX: 2200,
}),
WEALTH_RETURN: Object.freeze({ DUST_SHARE: 0.005, REOPEN_MULTIPLE: 10, ROLLING_DAYS: Object.freeze([1, 7, 30, 365]) }),
AUTO_BLUR: Object.freeze({
PRESETS_MIN: Object.freeze([5, 15, 30]),
NEVER_MIN: 525600,
}),
GLOW_TEMPO: Object.freeze({
brisk: Object.freeze({ MOVE_S: 11, PAUSE_MIN_S: 18, PAUSE_MAX_S: 40, ALWAYS_MULT: 1 }),
calm: Object.freeze({ MOVE_S: 24, PAUSE_MIN_S: 45, PAUSE_MAX_S: 90, ALWAYS_MULT: 2 }),
slow: Object.freeze({ MOVE_S: 36, PAUSE_MIN_S: 80, PAUSE_MAX_S: 140, ALWAYS_MULT: 3 }),
}),
GLOW_CORE_CHANCE: 0.35,
ATTENTION_RING: Object.freeze({ ROUND_MS: 3500, ROUNDS: 3 }),
TIMERS: Object.freeze({
OFF_SECONDS: 31536000,
SPOT_REFRESH: Object.freeze({
MIN_SECONDS: 60,
MAX_SECONDS: 3600,
PRESETS_SECONDS: Object.freeze([60, 300, 900]),
}),
BUS_POLL: Object.freeze({
MIN_SECONDS: 5,
MAX_SECONDS: 300,
PRESETS_SECONDS: Object.freeze([5, 15, 60]),
}),
}),
PRICE_HISTORY: Object.freeze({
NEAREST_MAX_SECONDS: 60,
CARRY_FORWARD_DAYS: Object.freeze({ crypto: 7, metal: 14, fiat: null }),
CLOSED_MARKET_DAYS: Object.freeze({ crypto: 0, metal: 4, fiat: 4 }),
WEEKEND_PRICE: Object.freeze({ crypto: true, metal: false, fiat: false }),
RECENT_CLOSE_DAYS: 14,
FX_MAX_AGE_DAYS: 7,
}),
API_USAGE: Object.freeze({ WARN_SHARE: 0.8 }),
HYGIENE: Object.freeze({
PRICE_GAP_DAYS: 7,
MAX_ROWS_PER_FINDING: 200,
OUTLIER_FACTOR: 3,
TROY_OUNCE_TOLERANCE: 0.2,
SOURCES_DISAGREE_SHARE: 0.08,
BOOKING_PRICE_DEVIATION: 0.2,
}),
BUS: Object.freeze({
RETENTION_HOURS: 24,
}),
INTEGRATION_LOG: Object.freeze({
RETENTION_DAYS: 90,
MAX_ROWS: 10_000,
DETAIL_MAX_CHARS: 300,
WINDOW_DAYS: 7,
AI_MAX_ROWS: 20,
}),
COMMANDS: Object.freeze({
CLAIM_SECONDS: 20,
COUNTDOWN_SECONDS: 5,
NOTICE_SECONDS: 3,
RETENTION_HOURS: 24,
}),
BACKUP: Object.freeze({
REMIND_AFTER_DAYS: 14,
}),
HEALTH_CHECK: Object.freeze({
REMIND_AFTER_DAYS: 30,
}),
AUTH: Object.freeze({
SESSION_TTL_MS: 12 * 60 * 60 * 1000,
COOKIE_NAME: 'assetapp_session',
LOGIN_PATH: '/login',
LOGIN_ATTEMPTS: Object.freeze({
FREE: 3,
BASE_LOCK_MS: 2000,
MAX_LOCK_MS: 5 * 60 * 1000,
}),
}),
ATTENTION: Object.freeze({
FADE_AFTER_DAYS: 1,
HIDE_AFTER_DAYS: 2,
}),
FOCUS: Object.freeze({
INTEREST_ACCOUNT: 'interest.focusAccount',
ROW: 'focus.row',
ROW_TTL_MS: 30_000,
}),
LICENSE: Object.freeze({
FILE_NAME: 'license.json',
FREE_FILE_NAME: 'free-license.json',
BUYER_FILE_NAME: 'buyer.json',
SUPPORT: Object.freeze({
ATTACHMENT_TYPES: Object.freeze(['image/png', 'image/jpeg', 'image/webp']),
MAX_ATTACHMENT_BYTES: 2 * 1024 * 1024,
RECIPIENT: '72304ea4d273d6c685aaa76342975e7a001190c9f15369ed3d3b303b79c8c74c',
RECIPIENT_NAME: 'Atreides Assets',
RELAYS: Object.freeze(['wss://nos.lol']),
MAX_TEXT: 4000,
MAX_EVENT_BYTES: 64 * 1024,
PUBLISH_TIMEOUT_MS: 10_000,
}),
INFO_LINKS: Object.freeze([
Object.freeze({ id: 'nostrHow', url: 'https://nostr.how/' }),
Object.freeze({ id: 'nostrOrg', url: 'https://nostr.org/' }),
Object.freeze({ id: 'alby', url: 'https://getalby.com/alby-extension' }),
Object.freeze({ id: 'nos2x', url: 'https://github.com/fiatjaf/nos2x' }),
Object.freeze({ id: 'lightning', url: 'https://lightning.network/' }),
]),
PAY_APPS: Object.freeze([
Object.freeze({ id: 'phoenix', name: 'Phoenix', group: 'easy', url: 'https://phoenix.acinq.co/' }),
Object.freeze({ id: 'pocket', name: 'Pocket', group: 'easy', url: 'https://pocketbitcoin.com/lightning' }),
Object.freeze({ id: 'strike', name: 'Strike', group: 'other', url: 'https://strike.me/' }),
Object.freeze({ id: 'zeus', name: 'Zeus', group: 'other', url: 'https://zeusln.com/' }),
Object.freeze({ id: 'bringin', name: 'Bringin', group: 'other', url: 'https://bringin.app/' }),
]),
FEATURE_IMAGES: 'img/features',
FEATURE_GROUPS: Object.freeze(['overview', 'records', 'prices', 'app', 'bitcoin']),
FEATURES: Object.freeze([
{ id: 'dashboard', group: 'overview', free: null }, { id: 'wealth', group: 'overview', free: null },
{ id: 'returns', group: 'overview', free: null }, { id: 'taxes', group: 'overview', has: 'tax' },
{ id: 'balances', group: 'records', free: null }, { id: 'interest', group: 'records', free: null },
{ id: 'import', group: 'records', has: 'import' }, { id: 'data', group: 'records', has: 'data' },
{ id: 'health', group: 'records', has: 'health' }, { id: 'metals', group: 'records', free: false },
{ id: 'priceHistory', group: 'prices', free: null },
{ id: 'sourceChoice', group: 'prices', has: 'sourceMatrix' }, { id: 'providerKeys', group: 'prices', has: 'providerKeys' },
{ id: 'portfolios', group: 'app', has: 'portfolios' }, { id: 'ai', group: 'app', has: 'ai' },
{ id: 'kiosk', group: 'app', has: 'kiosk' },
{ id: 'tracking', group: 'bitcoin', free: null }, { id: 'lightning', group: 'bitcoin', has: 'lightning' },
{ id: 'ownMempool', group: 'bitcoin', has: 'ownMempool' }, { id: 'signals', group: 'bitcoin', has: 'signals' },
{ id: 'netshare', group: 'bitcoin', has: 'netshare' },
].map(f => Object.freeze(f))),
SELLER_KEY: 'cfc6e67daaf080abbdcbacdbca587d54a3c18b119f2f05d7b087b859caa96a2f',
FORMAT: 1,
FORMATS: Object.freeze([1, 2]),
MESSAGE_PREFIX: 'assetapp-license-v1',
ISSUER_PREFIX: 'assetapp-issuer-v1',
UNLIMITED: 'unlimited',
LEVELS: Object.freeze(['full', 'free']),
FREE: Object.freeze({
MAX: Object.freeze({
assets: 1,
fiatAccounts: 1,
portfolios: 1,
valuations: 1,
}),
ASSETS: Object.freeze(['BTC']),
HAS: Object.freeze({
ai: true,
sourceMatrix: false,
lightning: false,
tax: false,
providerKeys: false,
ownMempool: false,
signals: false,
netshare: false,
portfolios: false,
import: false,
data: false,
health: false,
kiosk: false,
}),
}),
}),
EDITION: Object.freeze({
CLIENT: Object.freeze({
entry: 'client/edition.js',
starts: Object.freeze(['client/app.js', 'client/boot-gate.js', 'client/login.js']),
}),
SERVER: Object.freeze({
entry: 'server/edition.mjs',
starts: Object.freeze(['server/index.mjs', 'server/supervisor.mjs', 'server/mcp/stdio.mjs']),
}),
}),
PORTFOLIO: Object.freeze({
RESTART_EXIT_CODE: 75,
DEFAULT_BASE_CURRENCY: 'EUR',
BASE_CURRENCIES: Object.freeze([
'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'IDR', 'ILS', 'INR', 'ISK',
'JPY', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'SEK', 'SGD', 'THB', 'TRY', 'USD', 'ZAR',
]),
ACTIVE_KEY: 'portfolio_active',
SWITCH_KEY: 'portfolio_switching',
}),
SPLASH: Object.freeze({
MIN_MS: 3200,
FADE_MS: 900,
QUOTE_START_MS: 1800,
QUOTE_CHAR_MS: 40,
QUOTE_WORD_PAUSE_MS: 110,
}),
CONNECTIONS: Object.freeze({
TEST_TIMEOUT_MS: 8000,
RESULT_HOLD_MS: 6000,
RESULT_FADE_MS: 600,
}),
NWC: Object.freeze({
REQUEST_TIMEOUT_MS: 15000,
CLIENT_TIMEOUT_MS: 20000,
}),
LND: Object.freeze({
REQUEST_TIMEOUT_MS: 15000,
CLIENT_TIMEOUT_MS: 20000,
CHANNEL_LABEL: 'Lightning-kanaal',
}),
MARKET_SERVICE: Object.freeze({
SETTINGS_FILE_NAME: 'market-service.json',
URL: 'https://atreides.kamenier-hamer.nl',
APP_NAME: 'Atreides Services',
REQUEST_TIMEOUT_MS: 10000,
CLIENT_TIMEOUT_MS: 15000,
REGISTER_TTL_MS: 10 * 60_000,
MAX_SERIES_PAGES: 10,
INTRADAY_MAX_DAYS: 7,
MOMENT_WINDOW_MS: 10 * 60_000,
HISTORY_MAX_DAYS: 3650,
PROVIDER_KEY: 'atreides_services',
SPOT_TTL_MS: 30_000,
RETRY_PAUSE_MS: 5 * 60_000,
MAX_INSTRUMENTS_PER_SPOT: 100,
}),
AI: Object.freeze({
SERVER_NAME: 'assetapp',
SERVER_VERSION: '3',
PROTOCOL_VERSION: '2025-06-18',
AUDIT_PAGE_SIZE: 100,
PROPOSAL_PAGE_SIZE: 50,
DESTRUCTIVE_PROPOSAL_KINDS: Object.freeze(['account_delete']),
FIELD_CLASSES: Object.freeze(['plain', 'rate', 'quantityFiat', 'quantity', 'handle', 'chain', 'drop']),
BUCKET_LADDERS: Object.freeze({
COARSE: Object.freeze([1, 1.5, 2, 3, 5, 7]),
FINE: Object.freeze([1, 1.25, 1.6, 2, 2.5, 3.15, 4, 5, 6.3, 8]),
}),
BUCKET_MIN_VALUES: Object.freeze({ COARSE: 1000, FINE: 0 }),
SHARE_BUCKETS: Object.freeze([2, 4, 6, 8, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75,
80, 85, 90, 95, 100]),
SHARED_PREF_SCOPES: Object.freeze(['app', 'timers']),
PROPOSABLE_PREF_SCOPES: Object.freeze(['app', 'timers']),
LIMITS: Object.freeze({
MAX_ROWS_PER_LIST: 50,
MAX_TEXT_CHARS: 10000,
MAX_LOG_CHARS: 10000,
METRICS_WINDOW_HOURS: 24,
}),
}),
ROW_GUARD: false,
});
export const AUTH_KINDS = Object.freeze({
none: Object.freeze({ secret_fields: Object.freeze([]) }),
header_key: Object.freeze({ secret_fields: Object.freeze(['api_key']) }),
query_key: Object.freeze({ secret_fields: Object.freeze(['api_key']) }),
bearer: Object.freeze({ secret_fields: Object.freeze(['api_key']) }),
basic: Object.freeze({ secret_fields: Object.freeze(['api_key', 'api_secret']) }),
hmac: Object.freeze({ secret_fields: Object.freeze(['api_key', 'api_secret']) }),
});
export const ENUMS = Object.freeze({
ASSET_KIND: Object.freeze(['crypto', 'metal', 'fiat']),
ASSET_DISPLAY_UNIT: Object.freeze(['unit', 'subunit']),
METAL_FORM: Object.freeze(['coin', 'bar', 'other']),
ACCOUNT_KIND: Object.freeze(['exchange', 'wallet', 'lightning', 'card', 'bank', 'vault']),
TAX_CATEGORY: Object.freeze(['bank', 'overig']),
TAX_SCOPE: Object.freeze(['all', 'category', 'none']),
WEALTH_SCOPE: Object.freeze(['managed', 'none']),
EVENT_TYPE: Object.freeze(['buy', 'sell', 'transfer', 'interest', 'reservation']),
LEDGER_EVENT_TYPE: Object.freeze(['buy', 'sell', 'transfer', 'interest']),
EVENT_SOURCE: Object.freeze(['manual', 'import', 'reconcile', 'lightning']),
EVENT_PRICE_ORIGIN: Object.freeze(['paid', 'market_day', 'market_moment', 'none']),
VALUATION_SOURCE: Object.freeze(['onchain', 'manual', 'statement']),
CHANGE_OPERATION: Object.freeze(['update', 'delete']),
CHANGE_ORIGIN: Object.freeze(['manual', 'import', 'reconcile', 'interest', 'lightning',
'ai_proposal', 'merge', 'system']),
INTEREST_CADENCE: Object.freeze(['term_end', 'annual', 'quarterly', 'monthly', 'weekly']),
INTEREST_RATE_KIND: Object.freeze(['fixed', 'variable']),
INTEREST_CREDIT_STATUS: Object.freeze(['due', 'booked', 'dismissed']),
WALLET_SOURCE_KIND: Object.freeze(['xpub', 'address', 'nwc', 'lnd']),
ONCHAIN_SOURCE_KIND: Object.freeze(['xpub', 'address']),
SCRIPT_TYPE: Object.freeze(['p2tr', 'p2wpkh', 'p2sh-p2wpkh', 'p2pkh']),
SCAN_STATUS: Object.freeze(['success', 'partial', 'failed']),
RECONCILE_DIRECTION: Object.freeze(['in', 'out']),
WALLET_TX_LAYER: Object.freeze(['onchain', 'lightning']),
WALLET_TX_KIND: Object.freeze(['channel', 'self']),
RECONCILE_SUGGESTED: Object.freeze(['trade', 'transfer']),
RECONCILE_STATUS: Object.freeze(['pending', 'accepted_trade', 'accepted_transfer', 'accepted_interest', 'ignored', 'auto_linked']),
LINK_METHOD: Object.freeze(['exact', 'soft', 'manual']),
NOTIFICATION_KIND: Object.freeze(['reconcile', 'interest', 'valuation', 'schedule_ended', 'source_proposal', 'ai_proposal', 'reservation']),
CONNECTION_KIND: Object.freeze(['btc_explorer', 'lnd', 'nwc']),
THEME: Object.freeze(['dark', 'light']),
ACCENT: Object.freeze(['orange', 'magenta', 'lila', 'indigo', 'blue', 'cyan', 'teal', 'slate', 'sand']),
DEPTH: Object.freeze(['flat', 'deep']),
PROFILE: Object.freeze(['business', 'glass']),
GLOW: Object.freeze(['off', 'on']),
GLOW_SHAPE: Object.freeze(['blobs', 'bands']),
MOTION: Object.freeze(['off', 'step', 'always']),
GLOW_TEMPO: Object.freeze(['brisk', 'calm', 'slow']),
ROW_SELECTION: Object.freeze(['light', 'tint', 'pill']),
GLASS_RENDER: Object.freeze(['real', 'mimic']),
MWR_METHOD: Object.freeze(['dietz', 'irr']),
PERSONA: Object.freeze(['bitcoin', 'broad']),
MARKET_SPOT: Object.freeze(['off', 'fallback', 'first']),
REPEAT_ENTRY_KEY: Object.freeze(['ctrl_enter', 'enter']),
RESULT_SIGNS: Object.freeze(['none', 'negative', 'both']),
PRICE_RESOLUTION: Object.freeze(['day', 'timestamp']),
CONNECTION_STATUS: Object.freeze(['ok', 'fail']),
PRICE_SOURCE_STATUS: Object.freeze(['proposed', 'confirmed', 'manual']),
HMAC_HASH_ALGORITHM: Object.freeze(['SHA-256']),
HMAC_ENCODING: Object.freeze(['hex', 'base64']),
AI_CALL_OUTCOME: Object.freeze(['ok', 'error', 'refused']),
INTEGRATION_TARGET: Object.freeze(['service', 'provider']),
INTEGRATION_OUTCOME: Object.freeze(['fail', 'empty', 'refused']),
AI_MESSAGE_KIND: Object.freeze(['request', 'response']),
AI_SCOPE: Object.freeze(['none', 'read', 'write', 'control']),
COMMAND_KIND: Object.freeze(['navigate', 'locate']),
AI_PROPOSAL_KIND: Object.freeze(['price_point', 'account', 'event', 'account_delete', 'valuation', 'portfolio', 'preference', 'source_choice']),
SOURCE_CHOICE_ACTION: Object.freeze(['first', 'remove']),
QUOTE_UNIT: Object.freeze(['ozt']),
AI_PROPOSAL_STATUS: Object.freeze(['pending', 'accepted', 'rejected']),
COMMAND_STATE: Object.freeze(['open', 'claimed', 'done', 'rejected', 'expired']),
});
export const SYSTEM_PAGE = Object.freeze({
ROUTE: 'system',
TABS: Object.freeze({
PORTFOLIOS: 'portfolios',
SETTINGS: 'settings',
ASSETS: 'assets',
ACCOUNTS: 'accounts',
INTEGRATIONS: 'integrations',
IMPORT: 'import',
DATA: 'data',
HEALTH: 'health',
AI: 'ai',
LICENSE: 'license',
}),
});
export const EVENTS_PAGE = Object.freeze({
ROUTE: 'events',
TABS: Object.freeze({
ACCOUNTS: 'accounts',
VALUATIONS: 'valuations',
TRADES: 'trades',
TRANSFERS: 'transfers',
INTEREST: 'interest',
RESERVATIONS: 'reservations',
}),
});
export const CHARTS_PAGE = Object.freeze({
ROUTE: 'charts',
TABS: Object.freeze({
PRICE: 'price',
POWERLAW: 'pl',
WMA: 'wma',
MAYER: 'mayer',
}),
});
export const ROUTE_KEYS = Object.freeze({
HOME: 'home',
ACTIONS: 'actions',
EVENTS: EVENTS_PAGE.ROUTE,
WEALTH: 'wealth',
TAXES: 'taxes',
VALIDATION: 'validation',
TRACKING: 'tracking',
CONVERSION_BITCOIN: 'conversion/bitcoin',
PRICE_HISTORY: 'price-history',
CHARTS: CHARTS_PAGE.ROUTE,
SYSTEM: SYSTEM_PAGE.ROUTE,
GUIDE: 'guide',
});
const doel = (route, tab, sub) => Object.freeze({ route, tab: tab ?? null, sub: sub ?? null, element: null });
export const TARGETS = Object.freeze({
HOME: doel(ROUTE_KEYS.HOME),
ACTIONS: doel(ROUTE_KEYS.ACTIONS),
CHARTS: doel(ROUTE_KEYS.CHARTS),
TAXES: doel(ROUTE_KEYS.TAXES),
WEALTH: doel(ROUTE_KEYS.WEALTH),
TRACKING: doel(ROUTE_KEYS.TRACKING),
PRICE_HISTORY: doel(ROUTE_KEYS.PRICE_HISTORY),
CONVERSION: doel(ROUTE_KEYS.CONVERSION_BITCOIN),
GUIDE: doel(ROUTE_KEYS.GUIDE),
PORTFOLIOS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.PORTFOLIOS),
ASSETS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.ASSETS),
ACCOUNTS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.ACCOUNTS),
INTEGRATIONS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.INTEGRATIONS),
AI_ACTIVITY: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.AI),
SETTINGS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.SETTINGS),
LICENSE: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.LICENSE),
IMPORT: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.IMPORT),
DATA: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.DATA),
HEALTH: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.HEALTH),
PROVIDER_KEYS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.INTEGRATIONS, 'providers'),
SOURCE_PROPOSALS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.INTEGRATIONS, 'sources'),
CONNECTIONS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.INTEGRATIONS, 'connections'),
CHART_PRICE: doel(ROUTE_KEYS.CHARTS, CHARTS_PAGE.TABS.PRICE),
CHART_POWERLAW: doel(ROUTE_KEYS.CHARTS, CHARTS_PAGE.TABS.POWERLAW),
CHART_WMA: doel(ROUTE_KEYS.CHARTS, CHARTS_PAGE.TABS.WMA),
CHART_MAYER: doel(ROUTE_KEYS.CHARTS, CHARTS_PAGE.TABS.MAYER),
EVENT_ACCOUNTS: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.ACCOUNTS),
VALUATIONS: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.VALUATIONS),
TRADES: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.TRADES),
TRANSFERS: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.TRANSFERS),
INTEREST: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.INTEREST),
RESERVATIONS: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.RESERVATIONS),
});
export const NOTIFICATION_TARGETS = Object.freeze({
reconcile: TARGETS.ACTIONS,
interest: TARGETS.ACTIONS,
valuation: TARGETS.ACTIONS,
schedule_ended: TARGETS.ACCOUNTS,
source_proposal: TARGETS.SOURCE_PROPOSALS,
ai_proposal: TARGETS.ACTIONS,
reservation: TARGETS.ACTIONS,
});
export const NOTIFICATION_PRIORITY = Object.freeze([
'reconcile', 'valuation', 'reservation', 'interest', 'ai_proposal', 'source_proposal', 'schedule_ended',
]);
export const PLACE_LABELS = Object.freeze({
providers: 'integrations.tabProviders',
sources: 'integrations.tabSources',
connections: 'integrations.tabConnections',
[CHARTS_PAGE.TABS.PRICE]: 'signals.tabPrice',
[CHARTS_PAGE.TABS.POWERLAW]: 'signals.tabPl',
[CHARTS_PAGE.TABS.WMA]: 'signals.tabWma',
[CHARTS_PAGE.TABS.MAYER]: 'signals.tabMayer',
});
export function targetHash(target) {
if (!target || !target.route) throw new Error('A target without a route has no address.');
return `#/${[target.route, target.tab, target.sub].filter(Boolean).join('/')}`;
}
export function hashFromLocation(hash) {
const pad = String(hash || '').replace(/^#\/?/, '');
return pad ? `#/${pad}` : targetHash(TARGETS.HOME);
}
export function commandTooLate(expiresAt, nu = Date.now()) {
if (!expiresAt) return false;
const einde = Date.parse(expiresAt);
if (Number.isNaN(einde)) return false;
return einde - nu < CONFIG.COMMANDS.COUNTDOWN_SECONDS * 1000;
}
export function targetTab(target) {
if (!target || !target.tab) return null;
return [target.tab, target.sub].filter(Boolean).join('/');
}
export const CAPABILITIES = Object.freeze(['spot', 'daily_history', 'history_point']);
export const SOURCE_CAPABILITIES = Object.freeze(['spot', 'history']);
export const SOURCE_ENDPOINTS = Object.freeze({
spot: Object.freeze(['spot']),
history: Object.freeze(['daily_history', 'history_point']),
});
export function sourceCapabilityOf(capability) {
return SOURCE_CAPABILITIES.find(c => SOURCE_ENDPOINTS[c].includes(capability)) ?? null;
}
export const OUTPUT_FIELDS = Object.freeze(['time', 'price']);
export const TRANSFORMS = Object.freeze(['none', 'ms_to_s', 'to_number', 'last_per_day', 'troy_ounce_to_gram', 'iso_date_to_s', 'ms_to_s_local_close']);
export const HTTP_METHODS = Object.freeze(['GET', 'POST']);
export const ACCOUNT_SOURCE_KINDS = Object.freeze({
exchange: Object.freeze([]),
wallet: Object.freeze(['xpub', 'address']),
lightning: Object.freeze(['xpub', 'address', 'nwc', 'lnd']),
card: Object.freeze([]),
bank: Object.freeze([]),
vault: Object.freeze([]),
});
export const ACCOUNT_INTEREST_KINDS = Object.freeze(['exchange', 'card', 'bank']);
export const ACCOUNT_ASSET_KINDS = Object.freeze({
exchange: Object.freeze(['crypto', 'fiat']),
wallet: Object.freeze(['crypto']),
lightning: Object.freeze(['crypto']),
card: Object.freeze(['fiat']),
bank: Object.freeze(['fiat']),
vault: Object.freeze(['metal']),
});
export const TAX_CATEGORY_BY_ASSET_KIND = Object.freeze({ crypto: 'overig', metal: 'overig', fiat: 'bank' });
export const BALANCE_CAPTURE_KINDS = Object.freeze(['fiat']);
export function balanceCaptureOn(asset) {
if (!asset) return false;
if (asset.balanceCapture === 0 || asset.balanceCapture === 1) return asset.balanceCapture === 1;
return BALANCE_CAPTURE_KINDS.includes(asset.kind);
}
export const CONNECTION_PROVIDER = Object.freeze({ btc_explorer: 'mempool' });
export const ASSET_DEFINITIONS = Object.freeze({
BTC: Object.freeze({ symbol: 'BTC', name: 'Bitcoin', kind: 'crypto', unit: 'BTC', subunit: 'sat', decimals: 8, displayUnit: 'subunit' }),
ETH: Object.freeze({ symbol: 'ETH', name: 'Ethereum', kind: 'crypto', unit: 'ETH', subunit: null, decimals: 8, displayUnit: 'unit' }),
XAU: Object.freeze({ symbol: 'XAU', name: 'Goud', kind: 'metal', unit: 'ozt', subunit: 'g', decimals: 7, displayUnit: 'unit' }),
XAG: Object.freeze({ symbol: 'XAG', name: 'Zilver', kind: 'metal', unit: 'ozt', subunit: 'g', decimals: 7, displayUnit: 'unit' }),
});
export const PREFERENCES = Object.freeze({
app: Object.freeze({
theme: Object.freeze({ type: 'enum', allowed: ENUMS.THEME, initial: 'dark' }),
accent: Object.freeze({ type: 'enum', allowed: ENUMS.ACCENT, initial: 'blue' }),
depth: Object.freeze({ type: 'enum', allowed: ENUMS.DEPTH, initial: 'deep' }),
profile: Object.freeze({ type: 'enum', allowed: ENUMS.PROFILE, initial: 'business' }),
glow: Object.freeze({ type: 'enum', allowed: ENUMS.GLOW, initial: 'on' }),
glowshape: Object.freeze({ type: 'enum', allowed: ENUMS.GLOW_SHAPE, initial: 'blobs' }),
motion: Object.freeze({ type: 'enum', allowed: ENUMS.MOTION, initial: 'step' }),
rowsel: Object.freeze({ type: 'enum', allowed: ENUMS.ROW_SELECTION, initial: 'light' }),
glow_tempo: Object.freeze({ type: 'enum', allowed: ENUMS.GLOW_TEMPO, initial: 'calm' }),
glass_transparency: Object.freeze({ type: 'int', allowed: null, initial: 65, min: 0, max: 100 }),
glass_frost: Object.freeze({ type: 'int', allowed: null, initial: 3, min: 0, max: 50 }),
glass_blur: Object.freeze({ type: 'int', allowed: null, initial: 30, min: 0, max: 100 }),
glass_render: Object.freeze({ type: 'enum', allowed: ENUMS.GLASS_RENDER, initial: 'mimic' }),
blur: Object.freeze({ type: 'bool', allowed: null, initial: 0 }),
auto_blur_min: Object.freeze({ type: 'int', allowed: null, initial: 5, min: 1, max: CONFIG.AUTO_BLUR.NEVER_MIN }),
asset_show_name: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
asset_show_symbol: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
show_page_title: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
show_page_hint: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
kiosk: Object.freeze({ type: 'bool', allowed: null, initial: 0 }),
last_backup_at: Object.freeze({ type: 'string', allowed: null, initial: '' }),
last_health_check_at: Object.freeze({ type: 'string', allowed: null, initial: '' }),
start_year: Object.freeze({ type: 'int', allowed: null, initial: CONFIG.START_YEAR.AUTO, min: CONFIG.START_YEAR.AUTO, max: CONFIG.START_YEAR.MAX }),
mwr_method: Object.freeze({ type: 'enum', allowed: ENUMS.MWR_METHOD, initial: 'dietz' }),
repeat_entry_key: Object.freeze({ type: 'enum', allowed: ENUMS.REPEAT_ENTRY_KEY, initial: 'ctrl_enter' }),
persona: Object.freeze({ type: 'enum', allowed: ENUMS.PERSONA, initial: 'bitcoin' }),
setup_done: Object.freeze({ type: 'bool', allowed: null, initial: 0 }),
market_spot: Object.freeze({ type: 'enum', allowed: ENUMS.MARKET_SPOT, initial: 'off' }),
result_signs: Object.freeze({ type: 'enum', allowed: ENUMS.RESULT_SIGNS, initial: 'negative' }),
}),
timers: Object.freeze({
spot_refresh_seconds: Object.freeze({ type: 'int', allowed: null, initial: CONFIG.TIMERS.SPOT_REFRESH.MIN_SECONDS, min: CONFIG.TIMERS.SPOT_REFRESH.MIN_SECONDS, max: CONFIG.TIMERS.OFF_SECONDS }),
bus_poll_seconds: Object.freeze({ type: 'int', allowed: null, initial: CONFIG.TIMERS.BUS_POLL.MIN_SECONDS, min: CONFIG.TIMERS.BUS_POLL.MIN_SECONDS, max: CONFIG.TIMERS.OFF_SECONDS }),
}),
ai: Object.freeze({
enabled: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
allow_read: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
allow_raw: Object.freeze({ type: 'bool', allowed: null, initial: 0 }),
allow_write: Object.freeze({ type: 'bool', allowed: null, initial: 0 }),
seen_call_id: Object.freeze({ type: 'int', allowed: null, initial: 0, min: 0 }),
allow_control: Object.freeze({ type: 'bool', allowed: null, initial: 0 }),
bucket_shares: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
bucket_totals: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
bucket_amounts: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
bucket_quantities: Object.freeze({ type: 'bool', allowed: null, initial: 1 }),
}),
});
export const NETWORK = Object.freeze({ MAX_SUPPLY_BTC: 21_000_000 });
export const METRICS = Object.freeze({
WINDOW_MINUTES: 60,
FAIL_WARN: 0.05,
FAIL_BAD: 0.20,
});
export const CLOCKS = Object.freeze({
LIST_MAX: 4,
OPENING_WINDOW_MINUTES: 15,
CITIES: Object.freeze([
Object.freeze({ key: 'amsterdam', name: 'Amsterdam', tz: 'Europe/Amsterdam' }),
Object.freeze({ key: 'london', name: 'Londen', tz: 'Europe/London' }),
Object.freeze({ key: 'frankfurt', name: 'Frankfurt', tz: 'Europe/Berlin' }),
Object.freeze({ key: 'newyork', name: 'New York', tz: 'America/New_York' }),
Object.freeze({ key: 'losangeles', name: 'Los Angeles', tz: 'America/Los_Angeles' }),
Object.freeze({ key: 'saopaulo', name: 'São Paulo', tz: 'America/Sao_Paulo' }),
Object.freeze({ key: 'tokyo', name: 'Tokyo', tz: 'Asia/Tokyo' }),
Object.freeze({ key: 'hongkong', name: 'Hongkong', tz: 'Asia/Hong_Kong' }),
Object.freeze({ key: 'shanghai', name: 'Shanghai', tz: 'Asia/Shanghai' }),
Object.freeze({ key: 'singapore', name: 'Singapore', tz: 'Asia/Singapore' }),
Object.freeze({ key: 'dubai', name: 'Dubai', tz: 'Asia/Dubai' }),
Object.freeze({ key: 'mumbai', name: 'Mumbai', tz: 'Asia/Kolkata' }),
Object.freeze({ key: 'sydney', name: 'Sydney', tz: 'Australia/Sydney' }),
]),
EXCHANGES: Object.freeze([
Object.freeze({ key: 'tse', name: 'Tokyo (TSE)', tz: 'Asia/Tokyo', open: '09:00', close: '15:00', days: [1, 2, 3, 4, 5] }),
Object.freeze({ key: 'hkex', name: 'Hongkong (HKEX)', tz: 'Asia/Hong_Kong', open: '09:30', close: '16:00', days: [1, 2, 3, 4, 5] }),
Object.freeze({ key: 'sse', name: 'Shanghai (SSE)', tz: 'Asia/Shanghai', open: '09:30', close: '15:00', days: [1, 2, 3, 4, 5] }),
Object.freeze({ key: 'lse', name: 'Londen (LSE)', tz: 'Europe/London', open: '08:00', close: '16:30', days: [1, 2, 3, 4, 5] }),
Object.freeze({ key: 'euronext', name: 'Euronext (AMS)', tz: 'Europe/Amsterdam', open: '09:00', close: '17:30', days: [1, 2, 3, 4, 5] }),
Object.freeze({ key: 'xetra', name: 'Xetra (FRA)', tz: 'Europe/Berlin', open: '09:00', close: '17:30', days: [1, 2, 3, 4, 5] }),
Object.freeze({ key: 'nyse', name: 'New York (NYSE)', tz: 'America/New_York', open: '09:30', close: '16:00', days: [1, 2, 3, 4, 5] }),
Object.freeze({ key: 'nasdaq', name: 'Nasdaq', tz: 'America/New_York', open: '09:30', close: '16:00', days: [1, 2, 3, 4, 5] }),
]),
});
export const DASHBOARD = Object.freeze({
GRID_COLUMNS: 12,
MIN_SPAN: 1,
MAX_SPAN: 12,
MIN_H: 1,
MAX_H: 16,
HEIGHT_STEP_REM: 3,
SETTLE_MS: 250,
LONG_PRESS_MS: 500,
LONG_PRESS_SLOP_PX: 6,
HEIGHT_MODEL: 6,
LAYOUT_MODEL: 1,
SKELETON_STEPS: 2,
REVEAL_MAX_MS: 4000,
SIZE_SPAN: Object.freeze({ s: 3, m: 4, l: 6, xl: 12 }),
WIDGETS: Object.freeze({
signal: Object.freeze({ defaultSpan: 4, instanceable: false, persona: 'bitcoin', label_key: 'dashboard.widget.signal', defaultH: 4 }),
notifications: Object.freeze({ defaultSpan: 4, instanceable: false, persona: null, label_key: 'dashboard.widget.notifications', defaultH: 3 }),
standing: Object.freeze({ defaultSpan: 4, instanceable: false, persona: null, label_key: 'dashboard.widget.standing', defaultH: 3 }),
metrics: Object.freeze({ defaultSpan: 3, instanceable: false, persona: null, label_key: 'dashboard.widget.metrics', defaultH: 1 }),
netshare: Object.freeze({ defaultSpan: 3, instanceable: false, persona: 'bitcoin', label_key: 'dashboard.widget.netshare', defaultH: 2 }),
spot: Object.freeze({ defaultSpan: 2, instanceable: true, persona: null, label_key: 'dashboard.widget.spot', defaultH: 2 }),
clocks: Object.freeze({ defaultSpan: 4, instanceable: false, persona: null, label_key: 'dashboard.widget.clocks', defaultH: 5 }),
return: Object.freeze({ defaultSpan: 4, instanceable: true, persona: null, label_key: 'dashboard.widget.return', defaultH: 4 }),
}),
DEFAULT_LAYOUT: Object.freeze([
Object.freeze({ instId: 'notifications', type: 'notifications', x: 8 }),
]),
});
export function assertConfig() {
for (const cap of CAPABILITIES) {
if (SOURCE_CAPABILITIES.filter(c => SOURCE_ENDPOINTS[c].includes(cap)).length !== 1) {
throw new Error(`Capability '${cap}' must fall under exactly one source choice. Startup aborted.`);
}
}
for (const kind of ENUMS.ONCHAIN_SOURCE_KIND) {
if (!ENUMS.WALLET_SOURCE_KIND.includes(kind)) {
throw new Error(`ONCHAIN_SOURCE_KIND '${kind}' is not in WALLET_SOURCE_KIND. Startup aborted.`);
}
}
for (const kind of ACCOUNT_INTEREST_KINDS) {
if (!ENUMS.ACCOUNT_KIND.includes(kind)) {
throw new Error(`ACCOUNT_INTEREST_KINDS '${kind}' is not an account kind. Startup aborted.`);
}
}
for (const kind of Object.keys(ACCOUNT_SOURCE_KINDS)) {
if (!ENUMS.ACCOUNT_KIND.includes(kind)) {
throw new Error(`ACCOUNT_SOURCE_KINDS '${kind}' is not an account kind. Startup aborted.`);
}
}
for (const kind of ENUMS.ACCOUNT_KIND) {
const soorten = ACCOUNT_ASSET_KINDS[kind];
if (!soorten || !soorten.length || soorten.some(s => !ENUMS.ASSET_KIND.includes(s))) {
throw new Error(`ACCOUNT_ASSET_KINDS '${kind}' is missing or names an unknown asset kind. Startup aborted.`);
}
}
if (!CONFIG.PORTFOLIO.BASE_CURRENCIES.includes(CONFIG.PORTFOLIO.DEFAULT_BASE_CURRENCY)) {
throw new Error(`PORTFOLIO.DEFAULT_BASE_CURRENCY '${CONFIG.PORTFOLIO.DEFAULT_BASE_CURRENCY}' is not in BASE_CURRENCIES. Startup aborted.`);
}
const essential = [
['PRICE_SOURCES.MEMPOOL_PUBLIC_BASE', CONFIG.PRICE_SOURCES.MEMPOOL_PUBLIC_BASE],
['PRICE_SOURCES.BITVAVO_BASE', CONFIG.PRICE_SOURCES.BITVAVO_BASE],
['PRICE_SOURCES.COINGECKO_BASE', CONFIG.PRICE_SOURCES.COINGECKO_BASE],
['PRICE_SOURCES.GOLDAPI_BASE', CONFIG.PRICE_SOURCES.GOLDAPI_BASE],
['PRICE_SOURCES.EULERPOOL_BASE', CONFIG.PRICE_SOURCES.EULERPOOL_BASE],
['PRICE_SOURCES.METALSDEV_BASE', CONFIG.PRICE_SOURCES.METALSDEV_BASE],
['PRICE_SOURCES.FRANKFURTER_BASE', CONFIG.PRICE_SOURCES.FRANKFURTER_BASE],
['METALS.GRAMS_PER_TROY_OUNCE', CONFIG.METALS.GRAMS_PER_TROY_OUNCE],
['METALS.SPOT_DEVIATION_WARN', CONFIG.METALS.SPOT_DEVIATION_WARN],
['CHART_LIBS.CHARTJS', CONFIG.CHART_LIBS.CHARTJS],
['CHART_LIBS.HAMMER', CONFIG.CHART_LIBS.HAMMER],
['CHART_LIBS.ZOOM', CONFIG.CHART_LIBS.ZOOM],
['SCHEMA_VERSION', CONFIG.SCHEMA_VERSION],
['MAX_ASSET_DECIMALS', CONFIG.MAX_ASSET_DECIMALS],
['PRICE_HISTORY.NEAREST_MAX_SECONDS', CONFIG.PRICE_HISTORY.NEAREST_MAX_SECONDS],
['PRICE_HISTORY.CARRY_FORWARD_DAYS.crypto', CONFIG.PRICE_HISTORY.CARRY_FORWARD_DAYS.crypto],
['PRICE_HISTORY.RECENT_CLOSE_DAYS', CONFIG.PRICE_HISTORY.RECENT_CLOSE_DAYS],
['PRICE_HISTORY.FX_MAX_AGE_DAYS', CONFIG.PRICE_HISTORY.FX_MAX_AGE_DAYS],
['API_USAGE.WARN_SHARE', CONFIG.API_USAGE.WARN_SHARE],
['START_YEAR.MIN', CONFIG.START_YEAR.MIN],
['START_YEAR.MAX', CONFIG.START_YEAR.MAX],
['TIMERS.SPOT_REFRESH.MIN_SECONDS', CONFIG.TIMERS.SPOT_REFRESH.MIN_SECONDS],
['TIMERS.BUS_POLL.MIN_SECONDS', CONFIG.TIMERS.BUS_POLL.MIN_SECONDS],
['BUS.RETENTION_HOURS', CONFIG.BUS.RETENTION_HOURS],
['COMMANDS.CLAIM_SECONDS', CONFIG.COMMANDS.CLAIM_SECONDS],
['COMMANDS.COUNTDOWN_SECONDS', CONFIG.COMMANDS.COUNTDOWN_SECONDS],
['TIMERS.OFF_SECONDS', CONFIG.TIMERS.OFF_SECONDS],
['LND.REQUEST_TIMEOUT_MS', CONFIG.LND.REQUEST_TIMEOUT_MS],
['LND.CLIENT_TIMEOUT_MS', CONFIG.LND.CLIENT_TIMEOUT_MS],
['NWC.REQUEST_TIMEOUT_MS', CONFIG.NWC.REQUEST_TIMEOUT_MS],
['NWC.CLIENT_TIMEOUT_MS', CONFIG.NWC.CLIENT_TIMEOUT_MS],
['MARKET_SERVICE.SETTINGS_FILE_NAME', CONFIG.MARKET_SERVICE.SETTINGS_FILE_NAME],
['MARKET_SERVICE.URL', CONFIG.MARKET_SERVICE.URL],
['LICENSE.FREE_FILE_NAME', CONFIG.LICENSE.FREE_FILE_NAME],
['MARKET_SERVICE.APP_NAME', CONFIG.MARKET_SERVICE.APP_NAME],
['MARKET_SERVICE.REQUEST_TIMEOUT_MS', CONFIG.MARKET_SERVICE.REQUEST_TIMEOUT_MS],
['MARKET_SERVICE.CLIENT_TIMEOUT_MS', CONFIG.MARKET_SERVICE.CLIENT_TIMEOUT_MS],
['MARKET_SERVICE.REGISTER_TTL_MS', CONFIG.MARKET_SERVICE.REGISTER_TTL_MS],
['MARKET_SERVICE.MAX_SERIES_PAGES', CONFIG.MARKET_SERVICE.MAX_SERIES_PAGES],
['MARKET_SERVICE.INTRADAY_MAX_DAYS', CONFIG.MARKET_SERVICE.INTRADAY_MAX_DAYS],
['MARKET_SERVICE.HISTORY_MAX_DAYS', CONFIG.MARKET_SERVICE.HISTORY_MAX_DAYS],
['MARKET_SERVICE.PROVIDER_KEY', CONFIG.MARKET_SERVICE.PROVIDER_KEY],
['MARKET_SERVICE.SPOT_TTL_MS', CONFIG.MARKET_SERVICE.SPOT_TTL_MS],
['MARKET_SERVICE.MAX_INSTRUMENTS_PER_SPOT', CONFIG.MARKET_SERVICE.MAX_INSTRUMENTS_PER_SPOT],
['AI.SERVER_NAME', CONFIG.AI.SERVER_NAME],
['AI.SERVER_VERSION', CONFIG.AI.SERVER_VERSION],
['AI.PROTOCOL_VERSION', CONFIG.AI.PROTOCOL_VERSION],
['AI.AUDIT_PAGE_SIZE', CONFIG.AI.AUDIT_PAGE_SIZE],
['AI.PROPOSAL_PAGE_SIZE', CONFIG.AI.PROPOSAL_PAGE_SIZE],
['AI.LIMITS.MAX_ROWS_PER_LIST', CONFIG.AI.LIMITS.MAX_ROWS_PER_LIST],
['AI.LIMITS.MAX_TEXT_CHARS', CONFIG.AI.LIMITS.MAX_TEXT_CHARS],
['AI.LIMITS.MAX_LOG_CHARS', CONFIG.AI.LIMITS.MAX_LOG_CHARS],
['AI.LIMITS.METRICS_WINDOW_HOURS', CONFIG.AI.LIMITS.METRICS_WINDOW_HOURS],
['AUTH.SESSION_TTL_MS', CONFIG.AUTH.SESSION_TTL_MS],
['AUTH.COOKIE_NAME', CONFIG.AUTH.COOKIE_NAME],
['AUTH.LOGIN_PATH', CONFIG.AUTH.LOGIN_PATH],
['AUTH.LOGIN_ATTEMPTS.FREE', CONFIG.AUTH.LOGIN_ATTEMPTS.FREE],
['AUTH.LOGIN_ATTEMPTS.BASE_LOCK_MS', CONFIG.AUTH.LOGIN_ATTEMPTS.BASE_LOCK_MS],
['AUTH.LOGIN_ATTEMPTS.MAX_LOCK_MS', CONFIG.AUTH.LOGIN_ATTEMPTS.MAX_LOCK_MS],
];
for (const [name, val] of essential) {
if (val === undefined || val === null || val === '') {
throw new Error(`Essential config missing: ${name}. Startup aborted.`);
}
}
}