103 lines
6.1 KiB
JavaScript
103 lines
6.1 KiB
JavaScript
// Generated, do not edit. sha256:28bf467843efd410b1dacbe64352649ff4b982578fe5215b9da5061ebec43621
|
|||
|
|
import * as db from '../db.js';
|
||
|
|
import { nowIso } from './_shared.js';
|
||
|
|
import { fail } from '../../shared/app-error.js';
|
||
|
|
import { assertSourceMatchesAccount } from './wallet-sources.js';
|
||
|
|
import { createLabel } from './labels.js';
|
||
|
|
import { toSearchName } from '../../shared/search-name.js';
|
||
|
|
import { CONFIG } from '../../shared/config.js';
|
||
|
|
import { compactMemo } from '../../shared/zap.js';
|
||
|
|
|
||
|
|
const UPSERT = Object.freeze({
|
||
|
|
onchain: `INSERT INTO WalletTransaction
|
||
|
|
(accountId, sourceId, layer, txid, direction, valueSats, feeSats, blockHeight, blockTime, address, createdAt)
|
||
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
||
|
|
ON CONFLICT (accountId, txid) WHERE layer = 'onchain' DO UPDATE SET
|
||
|
|
sourceId=excluded.sourceId, direction=excluded.direction, valueSats=excluded.valueSats,
|
||
|
|
feeSats=excluded.feeSats, blockHeight=excluded.blockHeight, blockTime=excluded.blockTime,
|
||
|
|
address=excluded.address`,
|
||
|
|
lightning: `INSERT INTO WalletTransaction
|
||
|
|
(accountId, sourceId, layer, txid, direction, valueSats, feeSats, blockHeight, blockTime, address, kind, memo, createdAt)
|
||
|
|
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||
|
|
ON CONFLICT (sourceId, txid) WHERE layer = 'lightning' DO UPDATE SET
|
||
|
|
direction=excluded.direction, valueSats=excluded.valueSats,
|
||
|
|
feeSats=excluded.feeSats, blockHeight=excluded.blockHeight, blockTime=excluded.blockTime,
|
||
|
|
address=excluded.address, kind=excluded.kind, memo=excluded.memo`,
|
||
|
|
});
|
||
|
|
|
||
|
|
const TWIN_LABEL = `UPDATE WalletTransaction SET labelId = (
|
||
|
|
SELECT twin.labelId FROM WalletTransaction twin
|
||
|
|
WHERE twin.layer = 'lightning' AND twin.accountId = WalletTransaction.accountId
|
||
|
|
AND twin.txid = WalletTransaction.txid AND twin.labelId IS NOT NULL
|
||
|
|
ORDER BY twin.id LIMIT 1)
|
||
|
|
WHERE layer = 'lightning' AND labelId IS NULL
|
||
|
|
AND EXISTS (SELECT 1 FROM WalletTransaction twin
|
||
|
|
WHERE twin.layer = 'lightning' AND twin.accountId = WalletTransaction.accountId
|
||
|
|
AND twin.txid = WalletTransaction.txid AND twin.labelId IS NOT NULL)`;
|
||
|
|
|
||
|
|
function assertLayerShape(t) {
|
||
|
|
if (!Object.hasOwn(UPSERT, t.layer)) throw new Error(`Unknown wallet movement layer: '${t.layer}'.`);
|
||
|
|
if (t.layer !== 'lightning') return;
|
||
|
|
if (!Number.isInteger(t.sourceId)) throw new Error('A lightning movement needs its source.');
|
||
|
|
if (!t.blockTime) throw new Error('A lightning movement needs the moment it settled.');
|
||
|
|
if (t.blockHeight != null) throw new Error('A lightning movement has no block height.');
|
||
|
|
if (t.kind != null && t.kind !== 'self') throw new Error(`A lightning movement is ordinary or 'self', not '${t.kind}'.`);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function upsertWalletTxs(list) {
|
||
|
|
for (const given of list) {
|
||
|
|
const t = { ...given, layer: given.layer ?? 'onchain' };
|
||
|
|
assertLayerShape(t);
|
||
|
|
assertSourceMatchesAccount('WalletTransaction', t.accountId, t.sourceId);
|
||
|
|
const velden = [t.accountId, t.sourceId ?? null, t.layer, t.txid, t.direction, t.valueSats, t.feeSats ?? null,
|
||
|
|
t.blockHeight ?? null, t.blockTime ?? null, t.address ?? null];
|
||
|
|
db.run(UPSERT[t.layer], t.layer === 'lightning'
|
||
|
|
? [...velden, t.kind ?? null, compactMemo(t.memo), nowIso()] : [...velden, nowIso()]);
|
||
|
|
}
|
||
|
|
if (list.some(t => t.layer === 'lightning')) db.run(TWIN_LABEL);
|
||
|
|
await db.persist();
|
||
|
|
}
|
||
|
|
export function listSourceTxs(sourceId) {
|
||
|
|
return db.all(`SELECT t.*, l.name AS label_name
|
||
|
|
FROM WalletTransaction t
|
||
|
|
LEFT JOIN Label l ON l.id = t.labelId
|
||
|
|
WHERE t.sourceId = ?
|
||
|
|
ORDER BY (t.blockTime IS NULL) DESC, t.blockTime DESC, t.id DESC`, [sourceId]);
|
||
|
|
}
|
||
|
|
const CHANNEL_ROWS = `accountId IN (SELECT value FROM json_each(?1)) AND layer = 'onchain'
|
||
|
|
AND txid IN (SELECT value FROM json_each(?2))`;
|
||
|
|
|
||
|
|
export async function labelChannelTxs(accountId, txids) {
|
||
|
|
if (!Number.isInteger(accountId)) throw new Error('labelChannelTxs needs an account id.');
|
||
|
|
if (!Array.isArray(txids) || txids.some(x => typeof x !== 'string' || !/^[0-9a-f]{64}$/.test(x))) {
|
||
|
|
throw new Error('labelChannelTxs takes a list of transaction ids in lowercase hex.');
|
||
|
|
}
|
||
|
|
if (!txids.length) return 0;
|
||
|
|
const lijst = JSON.stringify(txids);
|
||
|
|
const accounts = JSON.stringify([accountId, ...db.all(
|
||
|
|
`SELECT DISTINCT onchain.accountId FROM WalletSource onchain
|
||
|
|
JOIN WalletSource node ON node.connectionId = onchain.connectionId AND node.kind = 'lnd'
|
||
|
|
WHERE node.accountId = ? AND onchain.kind IN ('xpub','address') AND onchain.accountId <> ?`,
|
||
|
|
[accountId, accountId]).map(r => r.accountId)]);
|
||
|
|
db.run(`UPDATE WalletTransaction SET kind = 'channel' WHERE ${CHANNEL_ROWS} AND kind IS NULL`, [accounts, lijst]);
|
||
|
|
const kandidaten = db.one(`SELECT COUNT(*) AS n FROM WalletTransaction WHERE ${CHANNEL_ROWS} AND labelId IS NULL`, [accounts, lijst]).n;
|
||
|
|
if (!kandidaten) { await db.persist(); return 0; }
|
||
|
|
const naam = CONFIG.LND.CHANNEL_LABEL;
|
||
|
|
const label = db.one(`SELECT id FROM Label WHERE searchName = ?`, [toSearchName(naam)])
|
||
|
|
|| { id: await createLabel({ name: naam }) };
|
||
|
|
db.run(`UPDATE WalletTransaction SET labelId = ?3 WHERE ${CHANNEL_ROWS} AND labelId IS NULL`, [accounts, lijst, label.id]);
|
||
|
|
await db.persist();
|
||
|
|
return kandidaten;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function setTxLabels(ids, labelId) {
|
||
|
|
if (!Array.isArray(ids) || ids.length === 0) fail('accounts.labelNoTxs', null, 'No transactions given for the label.');
|
||
|
|
db.run(`UPDATE WalletTransaction SET labelId = ?1
|
||
|
|
WHERE id IN (SELECT value FROM json_each(?2))
|
||
|
|
OR (layer = 'lightning' AND EXISTS (
|
||
|
|
SELECT 1 FROM WalletTransaction chosen
|
||
|
|
WHERE chosen.id IN (SELECT value FROM json_each(?2)) AND chosen.layer = 'lightning'
|
||
|
|
AND chosen.accountId = WalletTransaction.accountId AND chosen.txid = WalletTransaction.txid))`,
|
||
|
|
[labelId ?? null, JSON.stringify(ids)]);
|
||
|
|
await db.persist();
|
||
|
|
}
|