Files
2026-10-29 11:57:29 +01:00

144 lines
6.1 KiB
JavaScript

// Generated, do not edit. sha256:c2c77c87420dd87960f52c5550a95a9491166b15f248300e49e13aae650240fc
import { nowUtcIso } from '../../shared/time.js';
import { fail } from '../../shared/app-error.js';
import { assetPositions, eventAccountIdsByAsset, eventAssetIdsByAccount, eventsCountByAccount,
eventsNetByAccount, firstTaxEventDate } from './events.js';
import { anchorAt, anchorsBetween, createValuation, firstAnchorDate, latestAnchors, listValuations,
updateValuation, valuationDiff, valuationGap, valuationReconcilable } from './valuations.js';
import { accountOnchainBalance } from './wallet-addresses.js';
import { lightningBalances } from './wallet-sources.js';
import { onchainAsset } from './tx-links.js';
export function positionAt(accountId, assetId, atIso) {
const anker = anchorAt(accountId, assetId, atIso);
if (!anker) return eventsNetByAccount(accountId, assetId, atIso);
const naAnker = eventsNetByAccount(accountId, assetId, atIso) - eventsNetByAccount(accountId, assetId, anker.rateAt);
return anker.balance + naAnker;
}
export function positionsByAsset(atIso, { ownOnly = false } = {}) {
const peil = atIso ?? nowUtcIso();
const correctie = new Map();
for (const anker of latestAnchors(peil, { ownOnly })) {
const diff = valuationDiff(anker);
if (diff) correctie.set(anker.assetId, (correctie.get(anker.assetId) || 0) + diff);
}
const rijen = assetPositions(peil, { ownOnly });
if (correctie.size === 0) return rijen;
return rijen.map(r => (correctie.has(r.id) ? { ...r, net: r.net + correctie.get(r.id) } : r));
}
export function unexplainedChange(accountId, assetId, fromIso, untilIso) {
const stand = positionAt(accountId, assetId, untilIso) - positionAt(accountId, assetId, fromIso);
const geboekt = eventsNetByAccount(accountId, assetId, untilIso) - eventsNetByAccount(accountId, assetId, fromIso);
return stand - geboekt;
}
export function firstPositionDate() {
const gebeurtenis = firstTaxEventDate();
const anker = firstAnchorDate();
if (!gebeurtenis) return anker;
if (!anker) return gebeurtenis;
return gebeurtenis < anker ? gebeurtenis : anker;
}
export function unexplainedByPair(fromIso, untilIso) {
const uit = [];
for (const anker of latestAnchors(untilIso)) {
const units = unexplainedChange(anker.accountId, anker.assetId, fromIso, untilIso);
if (!units) continue;
let openingUnits = 0;
for (const v of anchorsBetween(anker.accountId, anker.assetId, fromIso, untilIso)) {
if (valuationReconcilable(v) && !v.gapAcceptedAt) continue;
openingUnits += valuationGap(v) || 0;
}
uit.push({
accountId: anker.accountId, assetId: anker.assetId, units, openingUnits,
hasLedger: eventsCountByAccount(anker.accountId, anker.assetId, untilIso) > 0,
});
}
return uit;
}
export function assetPositionsByAccount(assetId, atIso) {
const peil = atIso ?? nowUtcIso();
const accounts = new Set(eventAccountIdsByAsset(assetId));
for (const anker of latestAnchors(peil)) if (anker.assetId === assetId) accounts.add(anker.accountId);
const uit = [];
for (const accountId of accounts) {
const saldo = positionAt(accountId, assetId, peil);
if (saldo !== 0) uit.push({ accountId, saldo });
}
uit.sort((a, b) => b.saldo - a.saldo || a.accountId - b.accountId);
const totaal = positionsByAsset(peil).find(r => r.id === assetId);
const rest = (totaal ? totaal.net : 0) - uit.reduce((s, r) => s + r.saldo, 0);
if (rest !== 0) uit.push({ accountId: null, saldo: rest });
return uit;
}
export function accountPositions(accountId, atIso) {
const peil = atIso ?? nowUtcIso();
const assets = new Set(eventAssetIdsByAccount(accountId));
for (const anker of latestAnchors(peil)) if (anker.accountId === accountId) assets.add(anker.assetId);
const uit = [];
for (const assetId of [...assets].sort((a, b) => a - b)) {
const saldo = positionAt(accountId, assetId, peil);
if (saldo !== 0) uit.push({ assetId, saldo });
}
return uit;
}
export function accountHoldings(accountIds = [], atIso) {
const peil = atIso ?? nowUtcIso();
const uit = [];
for (const accountId of accountIds) {
for (const { assetId, saldo } of accountPositions(accountId, peil)) {
const anker = anchorAt(accountId, assetId, peil);
uit.push({ accountId, assetId, saldo,
anchor: anker ? { valuedOn: anker.valuedOn, rateAt: anker.rateAt, balance: anker.balance } : null });
}
}
return uit;
}
function measuredNodeBalance(row) {
return accountOnchainBalance(row.accountId) + (Number(row.sats) || 0);
}
export function lightningBalanceDiffs() {
const asset = onchainAsset();
if (!asset) return [];
const uit = [];
for (const row of lightningBalances()) {
const measured = measuredNodeBalance(row);
const ledger = positionAt(row.accountId, asset.id, row.scannedAt);
if (measured !== ledger) {
uit.push({ accountId: row.accountId, account_name: row.account_name, assetId: asset.id,
measured, ledger, gap: measured - ledger, scannedAt: row.scannedAt });
}
}
return uit;
}
export async function recordLightningBalance(accountId, { valuedOn, fiatRate } = {}) {
const row = lightningBalances().find(r => r.accountId === Number(accountId));
const asset = onchainAsset();
if (!row || !asset) throw new Error(`Account ${accountId} has no updated lightning source.`);
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(valuedOn))) throw new Error(`A balance needs its day: '${valuedOn}'.`);
if (!(Number(fiatRate) > 0)) throw new Error(`A balance needs a rate above zero: '${fiatRate}'.`);
const v = { assetId: asset.id, accountId: row.accountId, valuedOn, balance: measuredNodeBalance(row),
fiatRate: Number(fiatRate), rateAt: row.scannedAt, source: 'onchain' };
const zelfdeDag = listValuations().find(x => x.accountId === v.accountId && x.assetId === v.assetId && x.valuedOn === valuedOn);
if (zelfdeDag && zelfdeDag.source !== 'onchain') {
fail('valuations.lightningSameDay', { date: valuedOn },
`There is already a balance on ${valuedOn} for this account; it is not overwritten.`);
}
if (zelfdeDag) { await updateValuation(zelfdeDag.id, v); return zelfdeDag.id; }
return createValuation(v);
}