// Generated, do not edit. sha256:b2711d1eac7d201be0c0b5dbcc1d76495b068fd4828670afac37d02cd756c98f import { t } from '../i18n/index.js'; import { listConnections, saveConnection, setActiveConnection, deleteConnection, recordConnectionStatus } from '../data/connections.js'; import { localDateTimeNl } from '../../shared/time.js'; import { CONFIG, ENUMS } from '../../shared/config.js'; import { originAllowedByCsp } from '../../shared/csp.js'; import * as dialog from '../ui/dialog.js'; import * as lnd from '../lightning/lnd.js'; import * as market from '../market-service.js'; import { appSetting, setAppSetting } from '../app-settings.js'; import { clearSpotCache } from '../price/providers.js'; import { fmtInt } from '../ui/format.js'; import { esc } from '../ui/escape.js'; import { locked } from '../license.js'; import { serviceState } from '../data/integration-log.js'; import { lastFailureHtml, openFailureLog } from '../integrations/failure-log.js'; const KINDS = ['btc_explorer', 'lnd']; const KIND_META = { btc_explorer: { label: 'connections.kindBtcExplorer', usedBy: 'connections.usedByBtcExplorer' }, lnd: { label: 'connections.kindLnd', usedBy: 'connections.usedByLnd' }, }; const explorerUrlsAtLoad = Promise.all([listConnections('btc_explorer')]) .then(([rows]) => rows.map(c => c.url)).catch(() => []); const liveResult = new Map(); const fadeTimers = new Map(); function clearFade(kind) { clearTimeout(fadeTimers.get(kind)); fadeTimers.delete(kind); } function scheduleFade(el, kind) { clearFade(kind); fadeTimers.set(kind, setTimeout(() => { if (!isOnsPaneel(el)) { liveResult.delete(kind); return; } const host = el.querySelector(`[data-result="${kind}"]`); if (host) { host.style.transitionDuration = `${CONFIG.CONNECTIONS.RESULT_FADE_MS}ms`; host.classList.add('is-fading'); } fadeTimers.set(kind, setTimeout(() => { liveResult.delete(kind); if (isOnsPaneel(el)) paintResult(el, kind); }, CONFIG.CONNECTIONS.RESULT_FADE_MS)); }, CONFIG.CONNECTIONS.RESULT_HOLD_MS)); } async function testConnection(conn) { if (conn.kind === 'btc_explorer') { if (!originAllowedByCsp(conn.url, location.origin, await explorerUrlsAtLoad)) { throw new Error(t('connections.cspBlocked')); } const ctl = new AbortController(); const timer = setTimeout(() => ctl.abort(), CONFIG.CONNECTIONS.TEST_TIMEOUT_MS); try { const res = await fetch(`${conn.url}/api/blocks/tip/height`, { signal: ctl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const height = Number((await res.text()).trim()); if (!Number.isFinite(height) || height <= 0) throw new Error(t('connections.badResponse')); return `${t('connections.blockHeight')} ${fmtInt(height)}`; } finally { clearTimeout(timer); } } if (conn.kind === 'lnd') { const r = await lnd.testLnd(conn.id); const sync = t(r.synced ? 'connections.lndSynced' : 'connections.lndNotSynced'); return `${r.alias ? `${esc(r.alias)} · ` : ''}${sync} · ${t('connections.blockHeight')} ${fmtInt(r.blockHeight)}`; } throw new Error(t('connections.testUnsupported')); } async function runTest(el, conn, { footer = false } = {}) { if (footer) { clearFade(conn.kind); liveResult.set(conn.kind, { pending: true }); paintResult(el, conn.kind); } try { const detail = await testConnection(conn); await recordConnectionStatus(conn.id, true); if (footer) liveResult.set(conn.kind, { ok: true, text: `${esc(conn.name)}: ${t('connections.statusOk')} · ${detail}` }); } catch (e) { await recordConnectionStatus(conn.id, false); if (footer) liveResult.set(conn.kind, { ok: false, text: `${esc(conn.name)}: ${t('connections.statusFail')}: ${esc(e.message)}` }); } if (!isOnsPaneel(el)) return; draw(el); if (footer) scheduleFade(el, conn.kind); } function paintResult(el, kind) { const host = el.querySelector(`[data-result="${kind}"]`); if (!host) return; host.style.transitionDuration = ''; const r = liveResult.get(kind); if (!r) { host.className = 'conn-result'; host.innerHTML = ''; return; } if (r.pending) { host.className = 'conn-result'; host.textContent = t('connections.testing'); return; } host.className = `conn-result ${r.ok ? 'is-ok' : 'is-fail'}`; host.innerHTML = r.text; } function dotHtml(c) { const cls = c.lastStatus === 'ok' ? ' dot-ok' : c.lastStatus === 'fail' ? ' dot-fail' : ''; const label = c.lastStatus === 'ok' ? t('connections.statusOk') : c.lastStatus === 'fail' ? t('connections.statusFail') : t('connections.notTested'); const when = c.lastCheckedAt ? ` · ${t('connections.lastChecked')} ${localDateTimeNl(c.lastCheckedAt)}` : ''; return ``; } function tileHtml(c) { const hover = c.isSystem ? '' : ` `; return `
${dotHtml(c)}
${esc(c.name)}
${esc(c.url)}
${hover}
`; } const KIND_FEATURE = Object.freeze({ btc_explorer: 'ownMempool', lnd: 'lightning', nwc: 'lightning' }); async function frameHtml(kind) { const list = await listConnections(kind); const opSlot = locked(KIND_FEATURE[kind]); if (opSlot && !list.length) return ''; const tiles = list.map(tileHtml).join(''); return `
${t(KIND_META[kind].label)}
${t(KIND_META[kind].usedBy)}
${tiles}
${opSlot ? '' : ``}
`; } const MARKET = 'market'; let marketStatus = null; function tokenLine(st) { if (st.token_source === 'license') return t('connections.marketTokenLicense', { tier: t(`license.level_${st.tier}`) }); if (st.token_source === 'manual') return t('connections.marketTokenManual'); return t(st.can_register ? 'connections.marketTokenPending' : 'connections.marketTokenNoLicense'); } async function marketFrameHtml() { const st = await market.status(); let shown = marketStatus; if (!shown && st.token_set) { try { const s = await serviceState(); if (s.ok != null) shown = { ok: s.ok, at: Date.parse(s.at) }; } catch { } } const cls = !st.token_set || !shown ? '' : shown.ok ? ' dot-ok' : ' dot-fail'; const label = !st.token_set ? t('connections.marketTokenUnset') : !shown ? t('connections.notTested') : t(shown.ok ? 'connections.statusOk' : 'connections.statusFail'); const when = shown ? ` · ${t('connections.lastChecked')} ${localDateTimeNl(new Date(shown.at).toISOString())}` : ''; const lastFailure = st.token_set ? await lastFailureHtml('service').catch(() => '') : ''; const url = st.override ? `${st.url} (${t('connections.marketOverrideUrl')})` : ''; const stand = [tokenLine(st), t('connections.marketSpotShort_' + appSetting('market_spot'))].join(' · '); return `
${t('connections.kindMarket')}
${t('connections.usedByMarket')}
Atreides Services
${url ? `
${esc(url)}
` : ''}
${esc(stand)}
${lastFailure}
`; } async function runMarketTest(el, { footer = false } = {}) { if (footer) { clearFade(MARKET); liveResult.set(MARKET, { pending: true }); paintResult(el, MARKET); } try { const r = await market.test(); marketStatus = { ok: true, at: Date.now() }; const detail = t('connections.marketTestOk', { app: r.app, version: r.version || '', n: fmtInt(r.instruments) }); if (footer) liveResult.set(MARKET, { ok: true, text: `${t('connections.statusOk')} · ${esc(detail)}` }); } catch (e) { marketStatus = { ok: false, at: Date.now() }; if (footer) liveResult.set(MARKET, { ok: false, text: `${t('connections.statusFail')}: ${esc(e.message)}` }); } if (!isOnsPaneel(el)) return; await draw(el); if (footer) scheduleFade(el, MARKET); } async function marketDialog(el) { const res = await dialog.form({ title: t('connections.marketDialogTitle'), confirmLabel: t('connections.save'), fields: [ { key: 'spot', label: t('connections.marketSpot'), type: 'select', value: appSetting('market_spot'), options: ENUMS.MARKET_SPOT.map(v => ({ value: v, label: t('connections.marketSpot_' + v) })) }, ], }); if (!res) return; if (res.spot && res.spot !== appSetting('market_spot')) { await setAppSetting('market_spot', res.spot); clearSpotCache(); } await draw(el); runMarketTest(el, { footer: true }); } const byId = async id => (await listConnections()).find(c => c.id === Number(id)); const activeOf = async kind => (await listConnections(kind)).find(c => c.isActive); async function profileDialog(el, kind, c = null) { const fields = [ { key: 'name', label: t('connections.name'), value: c ? c.name : '' }, { key: 'url', label: t(kind === 'lnd' ? 'connections.lndUrl' : 'connections.url'), value: c ? c.url : '', placeholder: kind === 'lnd' ? '10.8.0.1:8080' : 'https://…' }, ]; if (kind === 'lnd') { const keep = c ? t('connections.secretKeepHint') : ''; fields.push( { key: 'macaroon', label: t('connections.lndMacaroon'), type: 'password', placeholder: keep || '0201036c6e64…' }, { key: 'tls_cert', label: t('connections.lndTlsCert'), type: 'textarea', placeholder: keep || '-----BEGIN CERTIFICATE-----' }, ); } const res = await dialog.form({ title: c ? t('connections.editProfile') : t('connections.newProfile'), confirmLabel: t('connections.save'), fields, }); if (!res) return; if (kind === 'lnd' && res.url && !/^[a-z]+:\/\//i.test(res.url.trim())) res.url = `https://${res.url.trim()}`; const id = await saveConnection({ id: c ? c.id : null, kind, name: res.name, url: res.url }); if (kind === 'lnd') await lnd.setConfig(id, { url: res.url, macaroon: res.macaroon, tls_cert: res.tls_cert }); draw(el); } const isOnsPaneel = (el) => !!el && !!el.querySelector('[data-result]'); async function draw(el) { const [marketFrame, frames] = await Promise.all([marketFrameHtml(), Promise.all(KINDS.map(frameHtml))]); el.innerHTML = marketFrame + frames.join(''); for (const kind of [MARKET, ...KINDS]) paintResult(el, kind); el.onclick = async e => { try { if (e.target.closest('[data-market-edit]')) { await marketDialog(el); return; } if (e.target.closest('[data-market-test]')) { await runMarketTest(el, { footer: true }); return; } const ilog = e.target.closest('[data-ilog-open]'); if (ilog) { await openFailureLog(ilog.dataset.ilogOpen); return; } const edit = e.target.closest('[data-edit]'); if (edit) { const c = await byId(edit.dataset.edit); if (c) await profileDialog(el, c.kind, c); return; } const del = e.target.closest('[data-del]'); if (del) { const c = await byId(del.dataset.del); if (!c) return; if (!(await dialog.confirm({ message: t('connections.confirmDelete'), danger: true }))) return; if (c.kind === 'lnd') await lnd.purgeConfig(c.id); await deleteConnection(c.id); draw(el); if (c.isActive) { const a = await activeOf(c.kind); if (a) runTest(el, a); } return; } const nieuw = e.target.closest('[data-new]'); if (nieuw) { await profileDialog(el, nieuw.dataset.new); return; } const testBtn = e.target.closest('[data-test-active]'); if (testBtn) { const a = await activeOf(testBtn.dataset.testActive); if (a) await runTest(el, a, { footer: true }); return; } const tile = e.target.closest('[data-select]'); if (tile) { const c = await byId(tile.dataset.select); if (!c || c.isActive) return; await setActiveConnection(c.id); draw(el); runTest(el, c); return; } } catch (err) { dialog.alert({ title: t('dialog.error'), message: err.message }); } }; el.onkeydown = e => { if ((e.key === 'Enter' || e.key === ' ') && e.target.matches && e.target.matches('[data-select], [data-market-edit]')) { e.preventDefault(); e.target.click(); } }; } export async function render(el) { for (const kind of [MARKET, ...KINDS]) clearFade(kind); liveResult.clear(); await draw(el); const st = await market.status(); if (st.token_set || st.can_register) runMarketTest(el); for (const kind of KINDS) { for (const c of await listConnections(kind)) runTest(el, c); } }