198 lines
7.3 KiB
JavaScript
198 lines
7.3 KiB
JavaScript
// Generated, do not edit. sha256:20b466379c38e65d9cee77881aaa8890de723421f475561a2047de7bd3f03d95
|
|||
|
|
import * as lnd from './lnd.js';
|
||
|
|
|
||
|
|
function toHex(v) {
|
||
|
|
const s = String(v || '');
|
||
|
|
if (!s) return '';
|
||
|
|
if (/^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0) return s.toLowerCase();
|
||
|
|
const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
|
||
|
|
let h = '';
|
||
|
|
for (let i = 0; i < bin.length; i++) h += bin.charCodeAt(i).toString(16).padStart(2, '0');
|
||
|
|
return h;
|
||
|
|
}
|
||
|
|
|
||
|
|
const toSats = v => Math.round(Number(v || 0));
|
||
|
|
const secToIso = s => (Number(s) ? new Date(Number(s) * 1000).toISOString() : null);
|
||
|
|
const nsToIso = ns => (Number(ns) ? new Date(Math.floor(Number(ns) / 1e6)).toISOString() : null);
|
||
|
|
|
||
|
|
const PAGE = 200;
|
||
|
|
const MAX_PAGES = 50;
|
||
|
|
|
||
|
|
async function fetchPayments(connId) {
|
||
|
|
const out = [];
|
||
|
|
let offset = 0;
|
||
|
|
for (let p = 0; p < MAX_PAGES; p++) {
|
||
|
|
const res = await lnd.listPayments(connId, { max_payments: PAGE, index_offset: offset, include_incomplete: false });
|
||
|
|
const batch = (res && res.payments) || [];
|
||
|
|
out.push(...batch);
|
||
|
|
const next = Number(res && res.last_index_offset) || 0;
|
||
|
|
if (!batch.length || next <= offset) break;
|
||
|
|
offset = next;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchInvoices(connId) {
|
||
|
|
const out = [];
|
||
|
|
let offset = 0;
|
||
|
|
for (let p = 0; p < MAX_PAGES; p++) {
|
||
|
|
const res = await lnd.listInvoices(connId, { num_max_invoices: PAGE, index_offset: offset });
|
||
|
|
const batch = (res && res.invoices) || [];
|
||
|
|
out.push(...batch);
|
||
|
|
const next = Number(res && res.last_index_offset) || 0;
|
||
|
|
if (!batch.length || next <= offset) break;
|
||
|
|
offset = next;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function fetchForwarding(connId) {
|
||
|
|
const out = [];
|
||
|
|
let offset = 0;
|
||
|
|
for (let p = 0; p < MAX_PAGES; p++) {
|
||
|
|
const res = await lnd.forwardingHistory(connId, { num_max_events: PAGE, index_offset: offset });
|
||
|
|
const batch = (res && res.forwarding_events) || [];
|
||
|
|
out.push(...batch);
|
||
|
|
const next = Number(res && res.last_offset_index) || 0;
|
||
|
|
if (!batch.length || next <= offset) break;
|
||
|
|
offset = next;
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function normalizeMovements(connId) {
|
||
|
|
const [payments, invoices, forwards] = await Promise.all([
|
||
|
|
fetchPayments(connId), fetchInvoices(connId), fetchForwarding(connId),
|
||
|
|
]);
|
||
|
|
|
||
|
|
const okPayments = payments.filter(p => p.status === 'SUCCEEDED');
|
||
|
|
const okInvoices = invoices.filter(i => i.state === 'SETTLED' || i.settled === true);
|
||
|
|
|
||
|
|
const invoiceHashes = new Set(okInvoices.map(i => toHex(i.r_hash)));
|
||
|
|
const selfHashes = new Set(okPayments.map(p => toHex(p.payment_hash)).filter(h => invoiceHashes.has(h)));
|
||
|
|
|
||
|
|
const movements = [];
|
||
|
|
|
||
|
|
for (const p of okPayments) {
|
||
|
|
const hash = toHex(p.payment_hash);
|
||
|
|
const isSelf = selfHashes.has(hash);
|
||
|
|
movements.push({
|
||
|
|
kind: 'payment',
|
||
|
|
type: isSelf ? 'transfer' : 'sell',
|
||
|
|
direction: 'out',
|
||
|
|
hash,
|
||
|
|
at: nsToIso(p.creation_time_ns) || secToIso(p.creation_date),
|
||
|
|
sats: toSats(p.value_sat),
|
||
|
|
fee_sats: toSats(p.fee_sat),
|
||
|
|
self: isSelf,
|
||
|
|
memo: null,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const i of okInvoices) {
|
||
|
|
const hash = toHex(i.r_hash);
|
||
|
|
if (selfHashes.has(hash)) continue;
|
||
|
|
movements.push({
|
||
|
|
kind: 'invoice',
|
||
|
|
type: 'buy',
|
||
|
|
direction: 'in',
|
||
|
|
hash,
|
||
|
|
at: secToIso(i.settle_date) || secToIso(i.creation_date),
|
||
|
|
sats: toSats(i.amt_paid_sat || i.value),
|
||
|
|
fee_sats: 0,
|
||
|
|
self: false,
|
||
|
|
memo: i.memo || null,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const f of forwards) {
|
||
|
|
const fee = toSats(f.fee);
|
||
|
|
if (!fee) continue;
|
||
|
|
movements.push({
|
||
|
|
kind: 'forward',
|
||
|
|
type: 'interest',
|
||
|
|
direction: 'route',
|
||
|
|
hash: null,
|
||
|
|
ref: `fwd:${f.timestamp_ns || f.timestamp || ''}:${f.chan_id_in || ''}:${f.chan_id_out || ''}`,
|
||
|
|
at: nsToIso(f.timestamp_ns) || secToIso(f.timestamp),
|
||
|
|
sats: fee,
|
||
|
|
fee_sats: 0,
|
||
|
|
self: false,
|
||
|
|
memo: null,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
movements.sort((a, b) => (a.at || '').localeCompare(b.at || ''));
|
||
|
|
return movements;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function nwcMemo(x) {
|
||
|
|
const beschrijving = typeof x?.description === 'string' ? x.description : '';
|
||
|
|
const zap = x?.metadata && typeof x.metadata.nostr === 'object' ? x.metadata.nostr : null;
|
||
|
|
if (zap && !beschrijving.trim().startsWith('{')) return { kind: 9734, ...zap };
|
||
|
|
return beschrijving || (typeof x?.metadata?.comment === 'string' ? x.metadata.comment : null) || null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function normalizeNwcMovements(sourceId) {
|
||
|
|
const nwc = await import('./nwc.js');
|
||
|
|
const all = [];
|
||
|
|
let offset = 0;
|
||
|
|
for (let p = 0; p < MAX_PAGES; p++) {
|
||
|
|
const res = await nwc.listTransactions(sourceId, { limit: PAGE, offset, unpaid: false });
|
||
|
|
const batch = (res && res.transactions) || [];
|
||
|
|
all.push(...batch);
|
||
|
|
if (batch.length < PAGE) break;
|
||
|
|
offset += batch.length;
|
||
|
|
}
|
||
|
|
const inHashes = new Set(all.filter(x => x.type === 'incoming').map(x => String(x.payment_hash || '')).filter(Boolean));
|
||
|
|
const selfHashes = new Set(all.filter(x => x.type === 'outgoing').map(x => String(x.payment_hash || '')).filter(h => inHashes.has(h)));
|
||
|
|
const movements = [];
|
||
|
|
for (const x of all) {
|
||
|
|
const hash = String(x.payment_hash || '');
|
||
|
|
const isSelf = !!hash && selfHashes.has(hash);
|
||
|
|
if (x.type === 'incoming' && isSelf) continue;
|
||
|
|
movements.push({
|
||
|
|
kind: 'nwc',
|
||
|
|
type: isSelf ? 'transfer' : (x.type === 'incoming' ? 'buy' : 'sell'),
|
||
|
|
direction: x.type === 'incoming' ? 'in' : 'out',
|
||
|
|
hash: hash || null,
|
||
|
|
at: secToIso(x.settled_at) || secToIso(x.created_at),
|
||
|
|
sats: Math.round(Number(x.amount || 0) / 1000),
|
||
|
|
fee_sats: Math.round(Number(x.fees_paid || 0) / 1000),
|
||
|
|
self: isSelf,
|
||
|
|
memo: nwcMemo(x),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
movements.sort((a, b) => (a.at || '').localeCompare(b.at || ''));
|
||
|
|
return movements;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function nodeBalance(connId) {
|
||
|
|
const [chain, chan, chans] = await Promise.all([
|
||
|
|
lnd.blockchainBalance(connId), lnd.channelBalance(connId), lnd.listChannels(connId),
|
||
|
|
]);
|
||
|
|
const amt = a => toSats(a && typeof a === 'object' ? a.sat : a);
|
||
|
|
const onchainConfirmed = toSats(chain && chain.confirmed_balance);
|
||
|
|
const onchainUnconfirmed = toSats(chain && chain.unconfirmed_balance);
|
||
|
|
const channelLocal = (chans && Array.isArray(chans.channels) ? chans.channels : [])
|
||
|
|
.reduce((s, ch) => s + toSats(ch && ch.local_balance) + (ch && ch.initiator ? toSats(ch.commit_fee) : 0), 0);
|
||
|
|
const unsettledLocal = amt(chan && chan.unsettled_local_balance);
|
||
|
|
const pendingOpen = amt(chan && chan.pending_open_local_balance) || toSats(chan && chan.pending_open_balance);
|
||
|
|
const anchor = onchainConfirmed + channelLocal;
|
||
|
|
const inflight = onchainUnconfirmed + unsettledLocal + pendingOpen;
|
||
|
|
return { onchainConfirmed, onchainUnconfirmed, channelLocal, unsettledLocal, pendingOpen, anchor, inflight };
|
||
|
|
}
|
||
|
|
|
||
|
|
const ZERO_TXID = /^0+$/;
|
||
|
|
export async function channelTxids(connId) {
|
||
|
|
const [open, closed] = await Promise.all([lnd.listChannels(connId), lnd.listClosedChannels(connId)]);
|
||
|
|
const out = new Set();
|
||
|
|
const funding = point => String(point || '').split(':')[0].toLowerCase();
|
||
|
|
for (const ch of (open && open.channels) || []) if (funding(ch.channel_point)) out.add(funding(ch.channel_point));
|
||
|
|
for (const ch of (closed && closed.channels) || []) {
|
||
|
|
if (funding(ch.channel_point)) out.add(funding(ch.channel_point));
|
||
|
|
const sluit = String(ch.closing_tx_hash || '').toLowerCase();
|
||
|
|
if (sluit && !ZERO_TXID.test(sluit)) out.add(sluit);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|