// Generated, do not edit. sha256:405d455ffaf6423b7f59a7abc882b3d6e788cead63eb4b179c23c015dd404720 import { listSelectableAccounts } from '../data/accounts.js'; import { dayNl, localDate, localDateTimeNl } from '../../shared/time.js'; import { listPendingProposals, resolveProposalAsTrade, resolveProposalAsTransfer, resolveProposalAsInternal, resolveProposalAsInterest, ignoreProposal, deleteProposal, ignoreAllPendingProposals, deleteAllPendingProposals, revertProposal, listHandledProposals } from '../data/proposals.js'; import { syncAddressLinks } from '../data/tx-links.js'; import { listAllWalletSources } from '../data/wallet-sources.js'; import { t } from '../i18n/index.js'; import { assetLabelHtml } from '../ui/asset-label.js'; import * as dialog from '../ui/dialog.js'; import { fmtInt, val } from '../ui/format.js'; import { baseCurrency } from '../app-settings.js'; import { esc } from '../ui/escape.js'; import { taskCardHtml, nothingPendingHtml } from '../ui/task-card.js'; import { isOnchainSource, scanSource } from '../onchain.js'; import { openMatchPopover } from '../ui/match-popover.js'; import { onchainAsset } from '../data/tx-links.js'; import { spotOnDate, spotAtMoment } from '../ui/fiat.js'; import { priceUnitLabel, totalFromPrice, priceFromTotal, priceToBase, priceFromBase } from '../../shared/units.js'; import { expanders } from '../ui/expand.js'; import { bulkPlan, bulkCounts, isForward } from './reconcile-bulk.js'; import { memoLine } from '../lightning/memo-line.js'; let wizIdx = 0; export async function renderReconcile(host, onChanged) { const rerender = () => renderReconcile(host, onChanged); const refresh = () => (onChanged ? onChanged() : rerender()); await syncAddressLinks(); const pending = await listPendingProposals(); const handled = await listHandledProposals(); const expand = await expanders('reconcile', { drawer: false }); if (wizIdx >= pending.length) wizIdx = Math.max(0, pending.length - 1); const current = pending[wizIdx] || null; const actions = ``; const wizard = pending.length === 0 ? nothingPendingHtml() : `
${wizIdx + 1} / ${pending.length}
${recRow(current, true, current.id)}`; host.innerHTML = taskCardHtml({ title: t('home.reconcile'), actions, body: `
${wizard} ${drawerSection(pending, handled, current, expand)}`, }); const prev = host.querySelector('#rec-prev'); if (prev) prev.onclick = () => { wizIdx = Math.max(0, wizIdx - 1); rerender(); }; const next = host.querySelector('#rec-next'); if (next) next.onclick = () => { wizIdx = Math.min(pending.length - 1, wizIdx + 1); rerender(); }; expand.bind(host); const igAll = host.querySelector('#rec-ignore-all'); if (igAll) igAll.onclick = async () => { if (!(await dialog.confirm({ message: t('home.recIgnoreAllConfirm') }))) return; await ignoreAllPendingProposals(); refresh(); }; const bookAll = host.querySelector('#rec-book-all'); if (bookAll) bookAll.onclick = () => bookAllLightning(host, pending, refresh); const delAll = host.querySelector('#rec-delete-all'); if (delAll) delAll.onclick = async () => { if (!(await dialog.confirm({ message: t('home.recDeleteAllConfirm'), danger: true }))) return; await deleteAllPendingProposals(); refresh(); }; host.querySelectorAll('[data-prop]').forEach(row => { const id = Number(row.dataset.prop); const form = row.querySelector('.prop-form'); row.querySelectorAll('[data-act]').forEach(btn => btn.onclick = async () => { const act = btn.dataset.act; if (act === 'ignore') { await ignoreProposal(id); refresh(); return; } if (act === 'internal' || act === 'interest') { try { await (act === 'internal' ? resolveProposalAsInternal(id) : resolveProposalAsInterest(id)); refresh(); } catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); } return; } if (act === 'link') { openMatchPopover(btn, { kind: 'tx', id: Number(row.dataset.wtx), assetId: Number(row.dataset.asset), onChange: refresh }); return; } if (act === 'trade') { showTradeForm(refresh, pending.find(x => x.id === id), form); return; } await showTransferForm(refresh, id, form); }); }); host.querySelectorAll('[data-del-prop]').forEach(btn => btn.onclick = async () => { if (!(await dialog.confirm({ message: t('home.recDeleteConfirm'), danger: true }))) return; await deleteProposal(Number(btn.dataset.delProp)); refresh(); }); host.querySelectorAll('[data-revert]').forEach(btn => btn.onclick = async () => { if (!(await dialog.confirm({ message: t('home.recRevertConfirm') }))) return; try { await revertProposal(Number(btn.dataset.revert)); refresh(); } catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); } }); const rematch = host.querySelector('#rec-rematch'); if (rematch) rematch.onclick = () => runUpdate(host, refresh); return pending.length; } function shortTx(h) { return h ? `${h.slice(0, 8)}…${h.slice(-4)}` : ''; } function momentCel(p) { const bevestigd = !!p.tx_block_time; const wanneer = bevestigd ? p.tx_block_time : p.detectedAt; const uitleg = bevestigd ? t('home.recTxAt') : t('home.recDetectedAt'); return `(${esc(localDateTimeNl(wanneer))})`; } function recRow(p, wiz, currentId) { const dir = p.direction === 'in' ? t('home.recIn') : t('home.recOut'); const wtxAttrs = p.walletTransactionId ? ` data-wtx="${p.walletTransactionId}" data-asset="${p.assetId}"` : ''; const linkBtn = p.walletTransactionId ? `` : ''; const cls = wiz ? 'rec-item rec-wiz-item' : `rec-item${p.id === currentId ? ' is-current' : ''}`; const laag = p.tx_layer === 'lightning' ? ` · ${t('home.recLightning')}${p.via_wallet != null ? ` ${t('home.recVia', { wallet: p.via_wallet || t('home.recViaNwc') })}` : ''}` : ''; const intern = p.tx_kind ? ` · ${t(`home.recInternal_${p.tx_kind}`)}` : ''; const internLabel = p.tx_node_account ? t('home.recAsChannelTransfer', { account: p.tx_node_account }) : t('home.recAsInternal'); const internBtn = p.tx_kind ? `` : ''; const forward = isForward(p); const renteBtn = forward ? `` : ''; const eerste = p.tx_kind || forward; const memo = memoLine(p.tx_memo, p.direction, 120); const memoHtml = memo ? `
${esc(memo.text)}
` : ''; return `
${val(`${fmtInt(p.amount)} sat`)} ${assetLabelHtml(p)} ${dir}${esc(laag)}${esc(intern)} · ${esc(p.account_name)} ${momentCel(p)}${p.transactionHash ? ` · ${esc(shortTx(p.transactionHash))}` : ''}${memoHtml}
${linkBtn} ${internBtn}${renteBtn}
`; } function drawerSection(pending, handled, current, expand) { const total = pending.length + handled.length; const teBoeken = bulkPlan(pending).filter(s => s.action !== 'skip').length; const bulk = `
${teBoeken ? `` : ''}
`; const rows = total === 0 ? `
${t('home.recHandledNone')}
` : pending.map(p => recRow(p, false, current ? current.id : 0)).join('') + handled.map(handledRow).join(''); return `
${t('home.recAll')} (${total}) ${bulk}
${rows}
`; } const STATUS_BADGE = { accepted_trade: 'badge-pos', accepted_transfer: 'badge-pos', accepted_interest: 'badge-pos', ignored: 'badge-neutral', auto_linked: 'badge-outline', }; const REVERTIBLE = new Set(['accepted_trade', 'accepted_transfer', 'accepted_interest', 'ignored']); function handledRow(p) { const dir = p.direction === 'in' ? t('home.recIn') : t('home.recOut'); const badge = `${t('enums.reconcile_status.' + p.status)}`; const revert = REVERTIBLE.has(p.status) ? `` : ''; const del = p.status !== 'auto_linked' ? `` : ''; return `
${val(`${fmtInt(p.amount)} sat`)} ${assetLabelHtml(p)} ${dir} · ${esc(p.account_name)} ${momentCel(p)}${p.transactionHash ? ` · ${esc(shortTx(p.transactionHash))}` : ''}
${revert}${del}${badge}
`; } let updating = false; async function runUpdate(host, refresh) { if (updating) return; updating = true; const btn = host.querySelector('#rec-rematch'); const prog = host.querySelector('#rec-progress'); if (btn) btn.disabled = true; try { for (const s of (await listAllWalletSources()).filter(isOnchainSource)) { const label = s.name ? `${s.account_name} · ${s.name}` : s.account_name; if (prog) prog.textContent = `${t('home.updating')} ${label}`; try { await scanSource(s, txt => { if (prog) prog.textContent = `${label}: ${txt}`; }); } catch (e) { console.error('[reconcile-update]', e); } } await syncAddressLinks(); } finally { updating = false; } refresh(); } async function bookAllLightning(host, pending, refresh) { const plan = bulkPlan(pending); const n = bulkCounts(plan); const ok = await dialog.confirm({ message: t('home.recBookAllConfirm', { trade: n.trade, internal: n.internal, interest: n.interest, skipped: n.onchain }), }); if (!ok) return; const prog = host.querySelector('#rec-progress'); const teller = { booked: 0, noPrice: 0, failed: 0 }; const werk = plan.filter(s => s.action !== 'skip'); for (const [i, s] of werk.entries()) { if (prog) prog.textContent = t('home.recBookAllProgress', { i: i + 1, n: werk.length }); try { if (s.action === 'internal') { await resolveProposalAsInternal(s.id); } else if (s.action === 'interest') { await resolveProposalAsInterest(s.id); } else { const k = await koersVoor(s.proposal); if (!k) { teller.noPrice++; continue; } await resolveProposalAsTrade(s.id, { fiatPrice: k.price, priceOrigin: k.origin }); } teller.booked++; } catch (e) { teller.failed++; console.error('[reconcile-book-all]', e); } } await dialog.alert({ title: t('home.recBookAllDoneTitle'), message: t('home.recBookAllDone', teller) }); refresh(); } async function koersVoor(p) { const asset = await onchainAsset(); if (!asset || asset.id !== p.assetId) return null; const dag = localDate(p.tx_block_time || p.detectedAt); const rond = n => Math.round(n * 100) / 100; const moment = p.tx_block_time ? await spotAtMoment(asset, p.tx_block_time) : null; if (moment) return { price: rond(moment.price), origin: 'market_moment', at: moment.at, dag, asset }; const koers = await spotOnDate(asset, dag); return koers == null ? null : { price: rond(koers), origin: 'market_day', dag, asset }; } async function vulKoers(p, form) { const input = form.querySelector('[data-price]'); const hint = form.querySelector('[data-spot]'); hint.textContent = t('home.recPriceLoading'); try { const k = await koersVoor(p); if (!k) { hint.textContent = t('home.recPriceUnknown'); return null; } input.value = String(k.price); input.dataset.filled = input.value; input.dataset.origin = k.origin; input.dispatchEvent(new Event('input', { bubbles: true })); hint.textContent = k.origin === 'market_moment' ? t('home.recPriceAt', { time: localDateTimeNl(new Date(k.at).toISOString()), unit: priceUnitLabel(k.asset) }) : t('home.recPriceFrom', { date: dayNl(k.dag), unit: priceUnitLabel(k.asset) }); return k.price; } catch { hint.textContent = t('home.recPriceUnknown'); return null; } } function koppelBedragEnPrijs(p, form) { const prijsEl = form.querySelector('[data-price]'); const totaalEl = form.querySelector('[data-total]'); onchainAsset().then(asset => { if (!asset || asset.id !== p.assetId) return; const dec = asset.decimals; const rond = n => (n == null ? '' : String(Math.round(n * 100) / 100)); const uitPrijs = () => { const basis = totalFromPrice(p.amount, dec, priceToBase(prijsEl.value, asset)); totaalEl.value = rond(basis); }; const uitTotaal = () => { const prijs = priceFromTotal(totaalEl.value, p.amount, dec); prijsEl.value = prijs === null ? '' : rond(priceFromBase(prijs, asset)); }; prijsEl.addEventListener('input', uitPrijs); totaalEl.addEventListener('input', uitTotaal); if (prijsEl.value !== '') uitPrijs(); }).catch(() => { }); } function showTradeForm(refresh, p, form) { if (!p) return; form.classList.remove('hidden'); form.innerHTML = `
`; vulKoers(p, form); koppelBedragEnPrijs(p, form); form.querySelector('[data-confirm]').onclick = async () => { const err = form.querySelector('[data-err]'); err.textContent = ''; const prijsEl = form.querySelector('[data-price]'); const veld = prijsEl.value; const price = veld === '' ? await vulKoers(p, form) : parseFloat(veld); if (!Number.isFinite(price)) { err.textContent = t('home.recPriceRequired'); return; } const gevuld = prijsEl.dataset.filled; const priceOrigin = gevuld !== undefined && Number(gevuld) === price ? (prijsEl.dataset.origin || 'market_day') : 'paid'; try { await resolveProposalAsTrade(p.id, { fiatPrice: price, priceOrigin }); refresh(); } catch (e) { err.textContent = e.message; } }; } async function showTransferForm(refresh, id, form) { form.classList.remove('hidden'); const opts = (await listSelectableAccounts()).map(a => ``).join(''); form.innerHTML = `
`; form.querySelector('[data-confirm]').onclick = async () => { const err = form.querySelector('[data-err]'); err.textContent = ''; const other = Number(form.querySelector('[data-counter]').value); try { await resolveProposalAsTransfer(id, { other_account_id: other }); refresh(); } catch (e) { err.textContent = e.message; } }; }