// Generated, do not edit. sha256:51ab737fb6604c8493f238a8893184408226a1a90ddd465fcd92bf0040b5a50e
import { mergeCheck, mergeAccounts } from '../data/accounts-merge.js';
import { listAccounts, listSelectableAccounts, createAccount, updateAccount, deleteAccount } from '../data/accounts.js';
import { listConnections } from '../data/connections.js';
import { tableViewCfg } from '../data/preferences.js';
import {
listWalletSources, createWalletSource, updateWalletSource,
deleteWalletSource
} from '../data/wallet-sources.js';
import { t } from '../i18n/index.js';
import { listSchedulesForAccount } from '../data/interest-schedules.js';
import { CONFIG, ENUMS, ACCOUNT_SOURCE_KINDS, ACCOUNT_INTEREST_KINDS, TARGETS, targetHash } from '../../shared/config.js';
import { renderTable } from '../ui/table.js';
import { takeRowFocus } from '../ui/row-focus.js';
import { copyButton } from '../ui/copy-button.js';
import { fmtInt, pct } from '../ui/format.js';
import { addressInfo } from '../price/btc-explorer.js';
import * as dialog from '../ui/dialog.js';
import { openPopover, closePopover } from '../ui/popover.js';
import { MERGE } from '../ui/icons.js';
import { esc } from '../ui/escape.js';
import { counterText, limitFor, locked } from '../license.js';
let tableApi = null;
const trunc = s => (s && s.length > 22) ? `${s.slice(0, 12)}…${s.slice(-6)}` : (s || '');
let bankCount = 0;
function kindOpts(vals) {
const max = limitFor('fiatAccounts');
const full = max !== null && bankCount >= max && vals?.kind !== 'bank';
return ENUMS.ACCOUNT_KIND.filter(v => !(full && v === 'bank'))
.map(v => ({ value: v, label: t('enums.account_kind.' + v) }));
}
function taxCategorieOpts() {
return [{ value: '', label: t('accounts.taxCategoryAuto') },
...ENUMS.TAX_CATEGORY.map(v => ({ value: v, label: t('enums.tax_category.' + v) }))];
}
function taxScopeOpts() {
return [{ value: '', label: t('accounts.taxScopeAuto') },
...ENUMS.TAX_SCOPE.map(v => ({ value: v, label: t('enums.tax_scope.' + v) }))];
}
function wealthScopeOpts() {
return [{ value: '', label: t('accounts.wealthScopeAuto') },
...ENUMS.WEALTH_SCOPE.map(v => ({ value: v, label: t('enums.wealth_scope.' + v) }))];
}
export async function render(el) {
el.innerHTML = `
`;
await buildTable(el);
}
async function buildTable(el) {
const columns = [
{ key: 'name', label: t('accounts.name'), sortable: true, text: true,
edit: { type: 'text', placeholder: 'Ledger / Bitvavo / ING' } },
{ key: 'kind', label: t('accounts.kind'), sortable: true,
render: a => t('enums.account_kind.' + a.kind),
group: { value: a => a.kind, label: v => t('enums.account_kind.' + v) },
edit: { type: 'select', options: kindOpts } },
{ key: 'isOwnCustody', label: t('accounts.ownCustody'), align: 'center', sortable: true,
render: a => `${a.isOwnCustody ? t('accounts.yes') : t('accounts.no')}`,
group: { value: a => a.isOwnCustody ? t('accounts.yes') : t('accounts.no'), label: v => v },
edit: { type: 'checkbox' } },
{ key: 'scanEnabled', label: t('accounts.scanEnabled'), align: 'center', sortable: true,
render: a => `${a.scanEnabled ? t('accounts.yes') : t('accounts.no')}`,
group: { value: a => a.scanEnabled ? t('accounts.yes') : t('accounts.no'), label: v => v },
edit: { type: 'checkbox' } },
{ key: 'taxCategory', label: t('accounts.taxCategory'), sortable: true,
render: a => a.taxCategory
? t('enums.tax_category.' + a.taxCategory)
: `${t('accounts.taxCategoryAuto')}`,
group: { value: a => a.taxCategory || '', label: v => v ? t('enums.tax_category.' + v) : t('accounts.taxCategoryAuto') },
edit: { type: 'select', options: taxCategorieOpts } },
{ key: 'taxScope', label: t('accounts.taxScope'), sortable: true,
render: a => a.taxScope
? t('enums.tax_scope.' + a.taxScope)
: `${t('accounts.taxScopeAuto')}`,
group: { value: a => a.taxScope || '', label: v => v ? t('enums.tax_scope.' + v) : t('accounts.taxScopeAuto') },
edit: { type: 'select', options: taxScopeOpts } },
{ key: 'wealthScope', label: t('accounts.wealthScope'), sortable: true,
render: a => a.wealthScope
? t('enums.wealth_scope.' + a.wealthScope)
: `${t('accounts.wealthScopeAuto')}`,
group: { value: a => a.wealthScope || '', label: v => v ? t('enums.wealth_scope.' + v) : t('accounts.wealthScopeAuto') },
edit: { type: 'select', options: wealthScopeOpts } },
];
function toPayload(v) {
if (!String(v.name || '').trim()) throw new Error(t('accounts.nameRequired'));
return {
name: v.name.trim(), kind: v.kind,
isOwnCustody: v.isOwnCustody ? 1 : 0,
scanEnabled: v.scanEnabled ? 1 : 0,
taxCategory: v.taxCategory || null,
taxScope: v.taxScope || null,
wealthScope: v.wealthScope || null,
};
}
const rijen = await listAccounts();
bankCount = countBanks(rijen);
tableApi = renderTable(el.querySelector('#tbl'), {
columns, rows: rijen,
...(limitFor('fiatAccounts') !== null
? { toolbarNote: () => `${t('license.name_fiatAccounts')}: ${counterText('fiatAccounts', bankCount)}` } : {}),
...(await tableViewCfg('accounts', { key: 'name', dir: 'asc' })),
emptyText: t('accounts.none'),
noneLabel: t('table.none'),
collapsibleGroups: true,
newLabel: t('table.new'),
newDefaults: { kind: 'exchange', isOwnCustody: false, scanEnabled: false, taxCategory: '', taxScope: '', wealthScope: '' },
saveLabel: t('accounts.save'), cancelLabel: t('accounts.cancel'),
onCreate: async v => {
const id = await createAccount(toPayload(v));
const r = await listAccounts();
bankCount = countBanks(r);
tableApi.refresh(r, { locate: id });
},
onSave: async (id, v) => { await updateAccount(id, toPayload(v)); reload(); },
onDelete: async id => {
if (!(await dialog.confirm({ message: t('accounts.confirmDelete'), danger: true }))) return;
try { await deleteAccount(id); reload(); } catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); }
},
toolbarActions: [{ id: 'merge', icon: MERGE, label: t('accounts.merge'), title: t('accounts.mergeTitle') }],
onToolbarAction: (id, anchor) => { if (id === 'merge') openMergeMenu(anchor); },
rowExpansion: () => '',
onExpansionMount: (a, host) => mountExpansion(host, a.id),
});
tableApi.focusRow(takeRowFocus('accounts'));
}
async function openMergeMenu(anchor) {
const doelen = await listSelectableAccounts();
const bronnen = await listSelectableAccounts({ metGesloten: true });
if (!doelen.length || bronnen.length < 2) { dialog.alert({ title: t('accounts.merge'), message: t('accounts.mergeNeedTwo') }); return; }
const { el: pop, position } = openPopover(anchor, { className: 'new-menu merge-menu' });
const ph = ``;
const optionsHtml = (lijst, selected, exclude) => ph + lijst
.filter(a => a.id !== exclude)
.map(a => ``).join('');
pop.innerHTML = `
${t('accounts.mergeKeep')}
${t('accounts.mergeDrop')}
`;
position();
const keepSel = pop.querySelector('[data-merge-keep]');
const dropSel = pop.querySelector('[data-merge-drop]');
const statusEl = pop.querySelector('[data-merge-status]');
const goBtn = pop.querySelector('[data-merge-go]');
function rebuildOpposite(changed, other) {
const otherVal = other.value;
const lijst = other === keepSel ? doelen : bronnen;
other.innerHTML = optionsHtml(lijst, Number(otherVal) || null, Number(changed.value) || null);
other.value = otherVal;
}
async function refresh() {
const survivorId = Number(keepSel.value), victimId = Number(dropSel.value);
const chk = await mergeCheck(survivorId, victimId);
if (chk.ok) {
const c = chk.counts;
statusEl.className = 't-body-sm t-muted';
statusEl.textContent = t('accounts.mergeSummary')
.replace('{drop}', chk.victimName).replace('{keep}', chk.survivorName)
.replace('{sources}', c.sources).replace('{events}', c.events)
.replace('{schedules}', c.schedules).replace('{valuations}', c.valuations);
goBtn.disabled = false;
} else {
statusEl.className = 't-body-sm t-neg';
statusEl.textContent = t('accounts.mergeReason_' + chk.reason);
goBtn.disabled = true;
}
position();
}
keepSel.onchange = () => { rebuildOpposite(keepSel, dropSel); refresh(); };
dropSel.onchange = () => { rebuildOpposite(dropSel, keepSel); refresh(); };
refresh();
goBtn.onclick = async () => {
const survivorId = Number(keepSel.value), victimId = Number(dropSel.value);
closePopover();
if (!(await dialog.confirm({ message: t('accounts.mergeConfirm'), danger: true }))) return;
try {
await mergeAccounts(survivorId, victimId);
tableApi.refresh(await listAccounts(), { locate: survivorId });
dialog.alert({ title: t('accounts.merge'), message: t('accounts.mergeDone') });
} catch (e) {
dialog.alert({ title: t('dialog.error'), message: e.message });
}
};
}
const countBanks = rows => rows.filter(r => r.kind === 'bank').length;
async function reload() { const r = await listAccounts(); bankCount = countBanks(r); tableApi.refresh(r); }
async function mountExpansion(host, accountId) {
const account = (await listAccounts()).find(a => a.id === accountId);
if (!account) { host.innerHTML = ''; return; }
const srcAllowed = ACCOUNT_SOURCE_KINDS[account.kind] || [];
const hasInterest = ACCOUNT_INTEREST_KINDS.includes(account.kind);
const parts = [];
if (srcAllowed.length) parts.push('');
if (hasInterest) parts.push(await interestBlok(accountId));
if (!parts.length) {
host.innerHTML = `${t('accounts.noSourceKind')}
`;
return;
}
host.innerHTML = parts.join('');
const link = host.querySelector('[data-interest-focus]');
if (link) link.onclick = () => sessionStorage.setItem(CONFIG.FOCUS.INTEREST_ACCOUNT, String(accountId));
if (srcAllowed.length) await mountSources(host.querySelector('[data-src-host]'), accountId, srcAllowed);
}
function interestRegel(s) {
const delen = [esc(s.payout_symbol)];
if (s.rateKind === 'fixed') {
if (s.annualRate != null) delen.push(pct(s.annualRate));
if (s.creditCadence) delen.push(esc(t('enums.interest_cadence.' + s.creditCadence)));
if (s.compounding) delen.push(esc(t('interest.colCompounding')));
} else {
delen.push(esc(t('enums.interest_rate_kind.variable')));
}
delen.push(`${esc(s.startsOn)} → ${s.endsOn ? esc(s.endsOn) : esc(t('interest.endsOnOpen'))}`);
return `${delen.join(' · ')}
`;
}
async function interestBlok(accountId) {
const scheds = await listSchedulesForAccount(accountId);
const lijst = scheds.length
? scheds.map(interestRegel).join('')
: `${t('accounts.interestNone')}
`;
return ``;
}
async function testSource(host, accountId, id) {
const out = host.querySelector('[data-src-test]');
const s = (await listWalletSources(accountId)).find(x => x.id === id);
if (!out || !s) return;
out.className = 't-body-sm t-muted mt-sm';
out.textContent = t('accounts.testing');
try {
let html;
if (s.kind === 'nwc') {
const { testNwc } = await import('../lightning/nwc.js');
const r = await testNwc(s.id);
html = `${t('accounts.testValid')}${r.alias ? ' · ' + esc(r.alias) : ''}`;
} else if (s.kind === 'lnd') {
if (!s.connectionId) throw new Error(t('accounts.lndNoConnection'));
const { testLnd } = await import('../lightning/lnd.js');
const r = await testLnd(s.connectionId);
html = `${t('accounts.testValid')}${r.alias ? ' · ' + esc(r.alias) : ''} · ${t('accounts.lndHeight')} ${fmtInt(r.blockHeight)}`;
} else if (s.kind === 'xpub') {
const { firstReceiveAddress } = await import('../price/wallet-scanner.js');
const addr = firstReceiveAddress(s.value, s.scriptType);
const info = await addressInfo(addr);
html = `${t('accounts.testValid')} · ${esc(trunc(addr))} · ${fmtInt(info.transactionCount)} ${t('accounts.testTxs')}`;
} else {
const info = await addressInfo(s.value);
html = `${t('accounts.testValid')} · ${fmtInt(info.transactionCount)} ${t('accounts.testTxs')}`;
}
out.className = 't-body-sm t-pos mt-sm';
out.innerHTML = `${esc(s.name || t('accounts.colSourceName'))}: ${html}`;
} catch (e) {
console.error('[bron-test]', e);
const msg = (e && (e.message || e.name)) || String(e);
out.className = 't-body-sm t-neg mt-sm';
out.textContent = `${s.name || t('accounts.colSourceName')}: ${t('accounts.testFail')}: ${msg}`;
}
}
async function mountSources(host, accountId, allowed) {
host.innerHTML = ``;
const KIND_LABEL = { xpub: 'accounts.xpub', address: 'accounts.address', nwc: 'accounts.nwc', lnd: 'accounts.lnd' };
const LIGHTNING = new Set(['nwc', 'lnd']);
const srcKindOpts = (vals) => allowed
.filter(k => !(LIGHTNING.has(k) && locked('lightning') && vals?.kind !== k))
.map(k => ({ value: k, label: t(KIND_LABEL[k]) }));
const scriptTypeOpts = () => ENUMS.SCRIPT_TYPE.map(s => ({ value: s, label: t('accounts.script_' + s) }));
const lndConnOpts = async () => [{ value: '', label: t('accounts.noNode') },
...(await listConnections('lnd')).map(c => ({ value: c.id, label: c.name }))];
const ONCHAIN = new Set(['xpub', 'address']);
const toSrc = async (v, existing = null) => {
const name = String(v.name || '').trim();
const value = String(v.value || '').trim();
if (!name) throw new Error(t('accounts.sourceNameRequired'));
if (v.kind === 'lnd') {
const connectionId = Number(v.connectionId);
if (!connectionId) throw new Error(t('accounts.lndNoConnection'));
return { src: { kind: 'lnd', value: '', name, connectionId } };
}
if (v.kind === 'nwc') {
if (!value && existing) return { src: { kind: 'nwc', value: existing.value, name }, secret: null };
if (!value) throw new Error(t('accounts.sourceValueRequired'));
const { validateUri } = await import('../lightning/nwc.js');
const parsed = await validateUri(value);
return { src: { kind: 'nwc', value: parsed.walletPubkey, name }, secret: value };
}
if (!value) throw new Error(t('accounts.sourceValueRequired'));
const src = { kind: v.kind, value, name };
if (v.kind === 'xpub') src.scriptType = v.scriptType;
if (ONCHAIN.has(v.kind)) src.connectionId = Number(v.connectionId) || null;
return { src };
};
let api = null;
const reloadSrc = async () => api.refresh(await listWalletSources(accountId));
api = renderTable(host.querySelector('[data-src-table]'), {
title: t('accounts.sources'),
columns: [
{ key: 'name', label: t('accounts.colSourceName'), sortable: true, text: true,
render: s => s.name ? esc(s.name) : '-',
edit: { type: 'text', placeholder: t('accounts.sourceNamePh') } },
{ key: 'kind', label: t('accounts.colSourceKind'), sortable: true,
render: s => s.kind === 'xpub' ? 'xpub' : s.kind === 'nwc' ? 'NWC' : s.kind === 'lnd' ? 'LND' : t('accounts.address'),
edit: { type: 'select', options: srcKindOpts, reactive: true } },
{ key: 'value', label: t('accounts.colSourceValue'),
render: s => s.kind === 'nwc'
? `${t('accounts.nwcPubkey')} ${esc(trunc(s.value))}${copyButton(s.value)}`
: s.kind === 'lnd'
? `${t('accounts.lndVia')} ${esc(s.connection_name || t('accounts.lndNoConnection'))}`
: `${esc(trunc(s.value))}${copyButton(s.value)}`,
edit: { type: 'text', visible: v => v.kind !== 'lnd',
placeholder: v => v.kind === 'nwc'
? (v._new ? 'nostr+walletconnect://…' : t('accounts.nwcKeepPh'))
: 'xpub6… / zpub… / bc1q…',
value: r => r ? (r.kind === 'nwc' ? '' : r.value) : '' } },
{ key: 'connectionId', label: t('accounts.colConnection'),
render: s => s.kind === 'lnd' ? esc(s.connection_name || t('accounts.lndNoConnection'))
: ONCHAIN.has(s.kind) && s.connection_name
? `${t('accounts.onchainOfNode')} ${esc(s.connection_name)}`
: '-',
edit: { type: 'select', options: lndConnOpts, visible: v => v.kind === 'lnd' || ONCHAIN.has(v.kind),
value: r => r && r.connectionId != null ? r.connectionId : '' } },
{ key: 'scriptType', label: t('accounts.colScriptType'),
render: s => s.kind === 'xpub' ? t('accounts.script_' + s.scriptType) : '-',
edit: { type: 'select', options: scriptTypeOpts, visible: v => v.kind === 'xpub' } },
],
rows: await listWalletSources(accountId),
...(await tableViewCfg('accounts.sources', { key: 'name', dir: 'asc' })),
emptyText: t('accounts.noSources'),
newLabel: t('table.new'),
newDefaults: { name: '', kind: allowed[0], value: '', scriptType: 'p2wpkh', connectionId: '', _new: 1 },
saveLabel: t('accounts.save'), cancelLabel: t('accounts.cancel'),
onCreate: async v => {
const { src, secret } = await toSrc(v);
await createWalletSource({ accountId: accountId, ...src, secret });
reloadSrc();
},
onSave: async (id, v) => {
const existing = (await listWalletSources(accountId)).find(s => s.id === id) || null;
const { src, secret } = await toSrc(v, existing);
await updateWalletSource(id, { ...src, secret });
reloadSrc();
},
onDelete: async id => {
if (!(await dialog.confirm({ message: t('accounts.confirmDeleteSource'), danger: true }))) return;
try { await deleteWalletSource(id); reloadSrc(); } catch (e) { dialog.alert({ title: t('dialog.error'), message: e.message }); }
},
extraActions: [{ key: 'test', label: t('accounts.testSource') }],
onAction: (key, id) => { if (key === 'test') testSource(host, accountId, id); },
});
}