Versie 0.2.0
This commit is contained in:
commit
e77d8fd373
495 files changed
+81026
No files matched your search
@@ -0,0 +1,576 @@
|
||||
// Generated, do not edit. sha256:eb712800d03552fa4079bd778c098f3fc2fb75b87a3722d0d27803f76118c2b9
|
||||
import { describe, it, beforeEach, mock } from 'node:test';
|
||||
import { toSearchName } from '../../shared/search-name.js';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { createDbHarness } from '../helpers/db-node.mjs';
|
||||
|
||||
const harness = createDbHarness();
|
||||
mock.module('../../server/db.js', { namedExports: harness.exports });
|
||||
|
||||
const {
|
||||
deleteProposal, ignoreAllPendingProposals, ignoreProposal, listPendingProposals,
|
||||
resolveProposalAsInterest, resolveProposalAsInternal, resolveProposalAsTrade, resolveProposalAsTransfer,
|
||||
revertProposal, syncTxProposals
|
||||
} = await import('../../server/data/proposals.js');
|
||||
const { linkTxEvent } = await import('../../server/data/tx-links.js');
|
||||
const { eventsNetByAccount } = await import('../../server/data/events.js');
|
||||
|
||||
let ctx;
|
||||
beforeEach(() => {
|
||||
harness.recreate();
|
||||
ctx = {
|
||||
btc: asset('BTC'),
|
||||
eigen: account('Hardware wallet', { ownCustody: true }),
|
||||
beurs: account('Exchange', { ownCustody: false }),
|
||||
};
|
||||
});
|
||||
|
||||
function asset(symbol) {
|
||||
harness.exports.run(
|
||||
`INSERT INTO Asset (symbol, name, searchName, kind, unit, subunit, decimals, displayUnit) VALUES (?,?,?,?,?,?,?,?)`, [symbol, symbol, toSearchName(symbol), 'crypto', symbol, null, 8, 'unit']);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
function account(naam, { ownCustody = true } = {}) {
|
||||
harness.exports.run(
|
||||
`INSERT INTO Account (name, searchName, kind, isOwnCustody, scanEnabled, isActive, closedAt) VALUES (?,?,?,?,?,1,NULL)`, [naam, toSearchName(naam), ownCustody ? 'wallet' : 'exchange', ownCustody ? 1 : 0, 1]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
function beweging({ account: acc = null, direction = 'in', valueSats = 250_000, txid = 'tx-1',
|
||||
blockTime = '2024-06-01T10:00:00', feeSats = null } = {}) {
|
||||
harness.exports.run(
|
||||
`INSERT INTO WalletTransaction (accountId, layer, txid, direction, valueSats, feeSats, address, blockHeight, blockTime, createdAt)
|
||||
VALUES (?,'onchain',?,?,?,?,?,?,?,?)`,
|
||||
[acc ?? ctx.eigen, txid, direction, valueSats, feeSats, null,
|
||||
blockTime ? 840_000 : null, blockTime, '2024-06-02T00:00:00']);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
const voorstellen = () => harness.exports.all(
|
||||
`SELECT id, walletTransactionId, amount, direction, suggestedType, status, resolvedEventId
|
||||
FROM ReconcileProposal ORDER BY id`);
|
||||
const events = () => harness.exports.all(`SELECT * FROM Event ORDER BY id`);
|
||||
|
||||
describe('syncTxProposals', () => {
|
||||
it('maakt één voorstel per beweging zonder koppeling', async () => {
|
||||
beweging({ txid: 'a' });
|
||||
beweging({ txid: 'b' });
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 2);
|
||||
assert.equal(voorstellen().length, 2);
|
||||
assert.ok(voorstellen().every(p => p.status === 'pending'));
|
||||
});
|
||||
|
||||
describe('lightning', () => {
|
||||
let nodes = 0;
|
||||
function bron(acc, kind) {
|
||||
let connectionId = null;
|
||||
if (kind === 'lnd') {
|
||||
harness.exports.run(`INSERT INTO Connection (kind, name, searchName, url, isActive, isSystem, createdAt, updatedAt)
|
||||
VALUES ('lnd', ?, ?, 'https://node.test', 1, 0, '2024-01-01', '2024-01-01')`, [`Node ${++nodes}`, `node ${nodes}`]);
|
||||
connectionId = harness.exports.lastId();
|
||||
}
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, connectionId, kind, value, name, createdAt)
|
||||
VALUES (?, ?, ?, ?, ?, '2026-01-01')`, [acc, connectionId, kind, `${kind}-${acc}`, kind === 'nwc' ? 'Zaps' : null]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
function betaling(acc, sourceId, txid = 'hash-zap', feeSats = 1) {
|
||||
harness.exports.run(`INSERT INTO WalletTransaction (accountId, sourceId, layer, txid, direction, valueSats, feeSats, blockTime, createdAt)
|
||||
VALUES (?, ?, 'lightning', ?, 'out', 21, ?, '2026-10-19T10:00:00Z', '2026-10-19')`, [acc, sourceId, txid, feeSats]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
|
||||
it('één voorstel voor een betaling op LND en NWC: vanaf de node', async () => {
|
||||
const lnd = betaling(ctx.eigen, bron(ctx.eigen, 'lnd'));
|
||||
betaling(ctx.eigen, bron(ctx.eigen, 'nwc'));
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 1);
|
||||
assert.deepEqual(voorstellen().map(p => p.walletTransactionId), [lnd]);
|
||||
});
|
||||
|
||||
it('een NWC-wallet op een account zonder LND-bron krijgt wel een voorstel', async () => {
|
||||
const nwc = betaling(ctx.eigen, bron(ctx.eigen, 'nwc'));
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 1);
|
||||
assert.deepEqual(voorstellen().map(p => p.walletTransactionId), [nwc]);
|
||||
});
|
||||
|
||||
it('een LND-node op een ander account telt niet voor deze NWC-wallet', async () => {
|
||||
bron(ctx.beurs, 'lnd');
|
||||
const nwc = betaling(ctx.eigen, bron(ctx.eigen, 'nwc'));
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
assert.deepEqual(voorstellen().map(p => p.walletTransactionId), [nwc]);
|
||||
});
|
||||
|
||||
it('de lijst zegt via welke NWC-wallet de betaling liep', async () => {
|
||||
betaling(ctx.eigen, bron(ctx.eigen, 'lnd'));
|
||||
betaling(ctx.eigen, bron(ctx.eigen, 'nwc'));
|
||||
betaling(ctx.eigen, bron(ctx.eigen, 'lnd'), 'alleen-node');
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const rijen = listPendingProposals().map(p => [p.transactionHash, p.tx_layer, p.via_wallet]);
|
||||
assert.deepEqual(rijen.sort(), [['alleen-node', 'lightning', null], ['hash-zap', 'lightning', 'Zaps']]);
|
||||
});
|
||||
|
||||
it('neemt de memo van de NWC-tweeling mee, en zet de zap in de notitie van de boeking', async () => {
|
||||
betaling(ctx.eigen, bron(ctx.eigen, 'lnd'));
|
||||
const nwc = betaling(ctx.eigen, bron(ctx.eigen, 'nwc'));
|
||||
const zap = JSON.stringify({ kind: 9734, pubkey: 'c'.repeat(64), content: 'Dank!',
|
||||
tags: [['p', '3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d']] });
|
||||
harness.exports.run(`UPDATE WalletTransaction SET memo = ? WHERE id = ?`, [zap, nwc]);
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
assert.equal(listPendingProposals()[0].tx_memo, zap);
|
||||
const eventId = await resolveProposalAsTrade(voorstellen()[0].id, { fiatPrice: 60_000 });
|
||||
assert.equal(events().find(x => x.id === eventId).note, 'zap to npub180cv…h6w6: "Dank!" · reconcile tx hash-zap');
|
||||
});
|
||||
|
||||
it('als verkoop zet hij de fee op de gebeurtenis', async () => {
|
||||
betaling(ctx.eigen, bron(ctx.eigen, 'lnd'), 'hash-zap', 3);
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const eventId = await resolveProposalAsTrade(voorstellen()[0].id, { fiatPrice: 60_000 });
|
||||
const e = events().find(x => x.id === eventId);
|
||||
assert.equal(e.type, 'sell');
|
||||
assert.equal(e.amount, 21);
|
||||
assert.equal(e.unitFee, 3);
|
||||
});
|
||||
});
|
||||
|
||||
it('slaat bewegingen over die al gekoppeld zijn', async () => {
|
||||
const tx = beweging();
|
||||
harness.exports.run(
|
||||
`INSERT INTO Event (assetId, type, occurredAt, amount, toAccountId, source, createdAt)
|
||||
VALUES (?,?,?,?,?,?,?)`, [ctx.btc, 'transfer', '2024-06-01T10:00:00', 250_000, ctx.eigen, 'manual', '2024-06-01T10:00:00']);
|
||||
const ev = harness.exports.lastId();
|
||||
await linkTxEvent(tx, ev, 'manual');
|
||||
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 0,
|
||||
'een beweging met een link is geen gat meer');
|
||||
});
|
||||
|
||||
it('stelt transfer voor bij eigen custody en trade bij een beurs', async () => {
|
||||
beweging({ account: ctx.eigen, txid: 'eigen' });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
assert.equal(voorstellen()[0].suggestedType, 'transfer');
|
||||
|
||||
beweging({ account: ctx.beurs, txid: 'beurs' });
|
||||
await syncTxProposals(ctx.beurs, ctx.btc);
|
||||
const bijBeurs = voorstellen().find(p => p.suggestedType === 'trade');
|
||||
assert.ok(bijBeurs, 'zonder eigen custody hoort een trade voorgesteld te worden');
|
||||
});
|
||||
|
||||
it('is idempotent: een herscan levert geen dubbele voorstellen', async () => {
|
||||
beweging();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
assert.equal(voorstellen().length, 1, 'pending wordt eerst opgeruimd, dan geregenereerd');
|
||||
});
|
||||
|
||||
it('raakt de voorstellen van een ánder account niet aan', async () => {
|
||||
beweging({ account: ctx.eigen, txid: 'a' });
|
||||
beweging({ account: ctx.beurs, txid: 'b' });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await syncTxProposals(ctx.beurs, ctx.btc);
|
||||
assert.equal(voorstellen().length, 2);
|
||||
});
|
||||
|
||||
describe('suppressie van genegeerde voorstellen', () => {
|
||||
it('laat een genegeerd voorstel niet terugkomen (match op walletTransactionId)', async () => {
|
||||
beweging();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await ignoreProposal(voorstellen()[0].id);
|
||||
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 0);
|
||||
assert.deepEqual(voorstellen().map(p => p.status), ['ignored'],
|
||||
'het genegeerde voorstel blijft, er komt niets nieuws bij');
|
||||
});
|
||||
|
||||
it('onderdrukt op (account, asset, richting, bedrag), ook zonder tx-verwijzing', async () => {
|
||||
harness.exports.run(
|
||||
`INSERT INTO ReconcileProposal
|
||||
(accountId, assetId, walletTransactionId, transactionHash, amount, direction, suggestedType, status, detectedAt)
|
||||
VALUES (?,?, NULL, NULL, ?, ?, 'transfer', 'ignored', ?)`,
|
||||
[ctx.eigen, ctx.btc, 777_000, 'in', '2024-05-01T00:00:00']);
|
||||
|
||||
beweging({ txid: 'nieuw-na-herimport', valueSats: 777_000, direction: 'in' });
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 0,
|
||||
'zelfde account/asset/richting/bedrag → nog steeds onderdrukt');
|
||||
});
|
||||
|
||||
it('een genegeerde beweging onderdrukt geen andere beweging met hetzelfde bedrag', async () => {
|
||||
beweging({ txid: 'a', valueSats: 21, direction: 'out' });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await ignoreProposal(voorstellen()[0].id);
|
||||
|
||||
const tweede = beweging({ txid: 'b', valueSats: 21, direction: 'out' });
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 1);
|
||||
assert.deepEqual(voorstellen().filter(p => p.status === 'pending').map(p => p.walletTransactionId), [tweede]);
|
||||
});
|
||||
|
||||
it('onderdrukt niet bij een ander bedrag', async () => {
|
||||
beweging({ txid: 'a', valueSats: 100 });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await ignoreProposal(voorstellen()[0].id);
|
||||
|
||||
beweging({ txid: 'b', valueSats: 200 });
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 1,
|
||||
'een ander bedrag is een ander gat');
|
||||
});
|
||||
|
||||
it('onderdrukt na hergebruik van een tx-id GEEN onverwante beweging meer', async () => {
|
||||
beweging({ txid: 'oud', valueSats: 100 });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await ignoreProposal(voorstellen()[0].id);
|
||||
assert.equal(voorstellen().length, 1);
|
||||
|
||||
harness.exports.run(`DELETE FROM WalletTransaction`);
|
||||
assert.deepEqual(voorstellen(), [], 'CASCADE: het voorstel gaat mee met zijn beweging');
|
||||
beweging({ txid: 'volstrekt-andere-tx', valueSats: 999_999 });
|
||||
|
||||
assert.equal(await syncTxProposals(ctx.eigen, ctx.btc), 1,
|
||||
'een onverwante beweging krijgt weer een gewoon voorstel');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listPendingProposals', () => {
|
||||
it('toont alleen pending, verrijkt met account- en assetnaam', async () => {
|
||||
beweging();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const lijst = listPendingProposals();
|
||||
assert.equal(lijst.length, 1);
|
||||
assert.equal(lijst[0].account_name, 'Hardware wallet');
|
||||
assert.equal(lijst[0].asset_symbol, 'BTC');
|
||||
assert.equal(lijst[0].asset_decimals, 8);
|
||||
});
|
||||
|
||||
it('verbergt een voorstel zodra de beweging gekoppeld raakt', async () => {
|
||||
const tx = beweging();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
assert.equal(listPendingProposals().length, 1);
|
||||
|
||||
harness.exports.run(
|
||||
`INSERT INTO Event (assetId, type, occurredAt, amount, toAccountId, source, createdAt)
|
||||
VALUES (?,?,?,?,?,?,?)`, [ctx.btc, 'transfer', '2024-06-01T10:00:00', 250_000, ctx.eigen, 'manual', '2024-06-01T10:00:00']);
|
||||
await linkTxEvent(tx, harness.exports.lastId(), 'manual');
|
||||
|
||||
assert.deepEqual(listPendingProposals(), [],
|
||||
'een gekoppelde beweging is geen openstaand gat meer');
|
||||
});
|
||||
|
||||
it('verbergt afgehandelde voorstellen', async () => {
|
||||
beweging();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await ignoreProposal(listPendingProposals()[0].id);
|
||||
assert.deepEqual(listPendingProposals(), []);
|
||||
});
|
||||
|
||||
it('draagt het moment van de TRANSACTIE mee en niet dat van het voorstel', async () => {
|
||||
beweging({ blockTime: '2021-03-04T12:00:00' });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const p = listPendingProposals()[0];
|
||||
assert.equal(p.tx_block_time, '2021-03-04T12:00:00');
|
||||
assert.notEqual(p.tx_block_time, p.detectedAt, 'anders bewijst deze test niets');
|
||||
});
|
||||
|
||||
it('houdt een ONBEVESTIGDE beweging in de lijst, zonder moment', async () => {
|
||||
beweging({ blockTime: null });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const lijst = listPendingProposals();
|
||||
assert.equal(lijst.length, 1, 'een onbevestigde beweging hoort nog steeds afgehandeld te worden');
|
||||
assert.equal(lijst[0].tx_block_time, null);
|
||||
assert.ok(lijst[0].detectedAt, 'dan is het moment van het voorstel het enige dat er is');
|
||||
});
|
||||
|
||||
it('houdt een GAT-voorstel in de lijst, dat aan geen transactie hangt', async () => {
|
||||
harness.exports.run(
|
||||
`INSERT INTO ReconcileProposal (accountId, assetId, walletTransactionId, amount, direction, suggestedType, status, detectedAt)
|
||||
VALUES (?,?,NULL,?,?,?,'pending',?)`,
|
||||
[ctx.eigen, ctx.btc, 100_000, 'in', 'transfer', '2024-06-02T00:00:00']);
|
||||
const lijst = listPendingProposals();
|
||||
assert.equal(lijst.length, 1);
|
||||
assert.equal(lijst[0].walletTransactionId, null);
|
||||
assert.equal(lijst[0].tx_block_time, null);
|
||||
});
|
||||
|
||||
it('sorteert op het moment van de beweging, nieuwste eerst', async () => {
|
||||
beweging({ txid: 'oud', blockTime: '2019-01-01T00:00:00' });
|
||||
beweging({ txid: 'nieuw', blockTime: '2025-01-01T00:00:00' });
|
||||
beweging({ txid: 'midden', blockTime: '2022-01-01T00:00:00' });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
assert.deepEqual(listPendingProposals().map(p => p.transactionHash), ['nieuw', 'midden', 'oud']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProposalAsTrade', () => {
|
||||
const eersteVoorstel = async (opts) => {
|
||||
beweging(opts);
|
||||
await syncTxProposals(opts?.account ?? ctx.eigen, ctx.btc);
|
||||
return voorstellen()[0].id;
|
||||
};
|
||||
|
||||
it('maakt van een inkomende beweging een buy op het account', async () => {
|
||||
const id = await eersteVoorstel({ direction: 'in' });
|
||||
const eventId = await resolveProposalAsTrade(id, { fiatPrice: 50_000, fiatFee: null, occurredAt: null });
|
||||
|
||||
const e = events()[0];
|
||||
assert.equal(e.id, eventId);
|
||||
assert.equal(e.type, 'buy');
|
||||
assert.equal(e.toAccountId, ctx.eigen);
|
||||
assert.equal(e.fromAccountId, null);
|
||||
assert.equal(e.amount, 250_000);
|
||||
assert.equal(e.source, 'reconcile');
|
||||
});
|
||||
|
||||
it('maakt van een uitgaande beweging een sell vanaf het account', async () => {
|
||||
const id = await eersteVoorstel({ direction: 'out' });
|
||||
await resolveProposalAsTrade(id, { fiatPrice: 50_000, fiatFee: null, occurredAt: null });
|
||||
const e = events()[0];
|
||||
assert.equal(e.type, 'sell');
|
||||
assert.equal(e.fromAccountId, ctx.eigen);
|
||||
assert.equal(e.toAccountId, null);
|
||||
});
|
||||
|
||||
it('eist een fiatPrice', async () => {
|
||||
const id = await eersteVoorstel();
|
||||
await assert.rejects(() => resolveProposalAsTrade(id, { fiatPrice: null }), /fiatPrice/);
|
||||
assert.deepEqual(events(), [], 'zonder prijs mag er geen event ontstaan');
|
||||
assert.equal(voorstellen()[0].status, 'pending', 'en het voorstel blijft open');
|
||||
});
|
||||
|
||||
it('zet het voorstel op accepted_trade met een verwijzing naar het event', async () => {
|
||||
const id = await eersteVoorstel();
|
||||
const eventId = await resolveProposalAsTrade(id, { fiatPrice: 1, fiatFee: null, occurredAt: null });
|
||||
const p = voorstellen()[0];
|
||||
assert.equal(p.status, 'accepted_trade');
|
||||
assert.equal(p.resolvedEventId, eventId);
|
||||
});
|
||||
|
||||
it('legt een soft-koppeling met de beweging', async () => {
|
||||
const id = await eersteVoorstel();
|
||||
const eventId = await resolveProposalAsTrade(id, { fiatPrice: 1, fiatFee: null, occurredAt: null });
|
||||
const l = harness.exports.all(`SELECT method, eventId FROM WalletTransactionEventLink`);
|
||||
assert.deepEqual(l, [{ method: 'soft', eventId: eventId }]);
|
||||
});
|
||||
|
||||
it('weigert een tweede afhandeling van hetzelfde voorstel', async () => {
|
||||
const id = await eersteVoorstel();
|
||||
await resolveProposalAsTrade(id, { fiatPrice: 1, fiatFee: null, occurredAt: null });
|
||||
await assert.rejects(() => resolveProposalAsTrade(id, { fiatPrice: 1 }), (e) => e.key === 'home.recHandled_accepted_trade');
|
||||
assert.equal(events().length, 1, 'geen tweede event');
|
||||
});
|
||||
|
||||
it('weigert een onbekend voorstel', async () => {
|
||||
await assert.rejects(() => resolveProposalAsTrade(9999, { fiatPrice: 1 }), (e) => e.key === 'home.recUnknownProposal');
|
||||
});
|
||||
|
||||
describe('welke datum wordt gekozen', () => {
|
||||
it('gebruikt een expliciete occurredAt als die er is', async () => {
|
||||
const id = await eersteVoorstel({ blockTime: '2024-06-01T10:00:00' });
|
||||
await resolveProposalAsTrade(id, { fiatPrice: 1, fiatFee: null, occurredAt: '2024-01-01T00:00:00' });
|
||||
assert.equal(events()[0].occurredAt, '2024-01-01T00:00:00');
|
||||
});
|
||||
|
||||
it('valt terug op de blockTime van de beweging', async () => {
|
||||
const id = await eersteVoorstel({ blockTime: '2024-06-01T10:00:00' });
|
||||
await resolveProposalAsTrade(id, { fiatPrice: 1, fiatFee: null, occurredAt: null });
|
||||
assert.equal(events()[0].occurredAt, '2024-06-01T10:00:00');
|
||||
});
|
||||
|
||||
it('valt daarna terug op het detectiemoment van het voorstel', async () => {
|
||||
const id = await eersteVoorstel({ blockTime: null });
|
||||
await resolveProposalAsTrade(id, { fiatPrice: 1, fiatFee: null, occurredAt: null });
|
||||
const detected = harness.exports.one(`SELECT detectedAt FROM ReconcileProposal`).detectedAt;
|
||||
assert.equal(events()[0].occurredAt, detected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProposalAsTransfer', () => {
|
||||
const eersteVoorstel = async (opts) => {
|
||||
beweging(opts);
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
return voorstellen()[0].id;
|
||||
};
|
||||
|
||||
it('zet bij uitgaand het eigen account als bron en de tegenrekening als bestemming', async () => {
|
||||
const id = await eersteVoorstel({ direction: 'out' });
|
||||
await resolveProposalAsTransfer(id, { other_account_id: ctx.beurs, unitFee: null, occurredAt: null });
|
||||
const e = events()[0];
|
||||
assert.equal(e.type, 'transfer');
|
||||
assert.equal(e.fromAccountId, ctx.eigen);
|
||||
assert.equal(e.toAccountId, ctx.beurs);
|
||||
});
|
||||
|
||||
it('draait de richting om bij inkomend', async () => {
|
||||
const id = await eersteVoorstel({ direction: 'in' });
|
||||
await resolveProposalAsTransfer(id, { other_account_id: ctx.beurs, unitFee: null, occurredAt: null });
|
||||
const e = events()[0];
|
||||
assert.equal(e.fromAccountId, ctx.beurs);
|
||||
assert.equal(e.toAccountId, ctx.eigen);
|
||||
});
|
||||
|
||||
it('eist een tegenrekening', async () => {
|
||||
const id = await eersteVoorstel();
|
||||
await assert.rejects(() => resolveProposalAsTransfer(id, { other_account_id: null }), (e) => e.key === 'home.recNoCounter');
|
||||
assert.deepEqual(events(), []);
|
||||
});
|
||||
|
||||
it('neemt de netwerkfee over bij een uitgaande beweging', async () => {
|
||||
const id = await eersteVoorstel({ direction: 'out', feeSats: 1_200 });
|
||||
await resolveProposalAsTransfer(id, { other_account_id: ctx.beurs, unitFee: null, occurredAt: null });
|
||||
assert.equal(events()[0].unitFee, 1_200);
|
||||
});
|
||||
|
||||
it('neemt de fee NIET over bij een inkomende beweging', async () => {
|
||||
const id = await eersteVoorstel({ direction: 'in', feeSats: 1_200 });
|
||||
await resolveProposalAsTransfer(id, { other_account_id: ctx.beurs, unitFee: null, occurredAt: null });
|
||||
assert.equal(events()[0].unitFee, null);
|
||||
});
|
||||
|
||||
it('laat een expliciete unitFee voorgaan op de netwerkfee', async () => {
|
||||
const id = await eersteVoorstel({ direction: 'out', feeSats: 1_200 });
|
||||
await resolveProposalAsTransfer(id, { other_account_id: ctx.beurs, unitFee: 99, occurredAt: null });
|
||||
assert.equal(events()[0].unitFee, 99);
|
||||
});
|
||||
|
||||
it('zet het voorstel op accepted_transfer', async () => {
|
||||
const id = await eersteVoorstel();
|
||||
const eventId = await resolveProposalAsTransfer(id, { other_account_id: ctx.beurs, unitFee: null, occurredAt: null });
|
||||
const p = voorstellen()[0];
|
||||
assert.equal(p.status, 'accepted_transfer');
|
||||
assert.equal(p.resolvedEventId, eventId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProposalAsInternal', () => {
|
||||
const voorstelVoor = async (wtx, kind) => {
|
||||
harness.exports.run(`UPDATE WalletTransaction SET kind = ? WHERE id = ?`, [kind, wtx]);
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
return voorstellen().find(p => p.walletTransactionId === wtx).id;
|
||||
};
|
||||
|
||||
it('boekt een kanaalopening als verplaatsing naar hetzelfde account: netto alleen de fee', async () => {
|
||||
const wtx = beweging({ direction: 'out', valueSats: 1_000_000, feeSats: 450 });
|
||||
const eventId = await resolveProposalAsInternal(await voorstelVoor(wtx, 'channel'));
|
||||
const e = events().find(x => x.id === eventId);
|
||||
assert.equal(e.type, 'transfer');
|
||||
assert.equal(e.fromAccountId, ctx.eigen);
|
||||
assert.equal(e.toAccountId, ctx.eigen);
|
||||
assert.equal(e.amount, 1_000_000);
|
||||
assert.equal(e.unitFee, 450);
|
||||
assert.equal(eventsNetByAccount(ctx.eigen, ctx.btc), -450);
|
||||
assert.equal(voorstellen()[0].status, 'accepted_transfer');
|
||||
assert.equal(harness.exports.one(`SELECT COUNT(*) AS n FROM WalletTransactionEventLink WHERE walletTransactionId = ?`, [wtx]).n, 1);
|
||||
});
|
||||
|
||||
it('een kanaalsluiting komt binnen zonder fee: netto nul', async () => {
|
||||
const wtx = beweging({ direction: 'in', valueSats: 990_000, feeSats: 300 });
|
||||
await resolveProposalAsInternal(await voorstelVoor(wtx, 'channel'));
|
||||
assert.equal(events()[0].unitFee, null);
|
||||
assert.equal(eventsNetByAccount(ctx.eigen, ctx.btc), 0);
|
||||
});
|
||||
|
||||
it('een kanaal van een gekoppelde wallet op een eigen account gaat naar het account van de node', async () => {
|
||||
harness.exports.run(`INSERT INTO Connection (kind, name, searchName, url, isActive, isSystem, createdAt, updatedAt)
|
||||
VALUES ('lnd', 'Node', 'node', 'https://node.test', 1, 0, '2024-01-01', '2024-01-01')`);
|
||||
const conn = harness.exports.lastId();
|
||||
const node = account('Lightning');
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, connectionId, kind, value, createdAt) VALUES (?, ?, 'lnd', '', '2026-01-01')`, [node, conn]);
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, connectionId, kind, value, scriptType, createdAt) VALUES (?, ?, 'xpub', 'zpub', 'p2wpkh', '2026-01-01')`, [ctx.eigen, conn]);
|
||||
const wtx = beweging({ direction: 'out', valueSats: 500_000, feeSats: 200 });
|
||||
const id = await voorstelVoor(wtx, 'channel');
|
||||
assert.equal(listPendingProposals()[0].tx_node_account, 'Lightning');
|
||||
const eventId = await resolveProposalAsInternal(id);
|
||||
const e = events().find(x => x.id === eventId);
|
||||
assert.deepEqual([e.type, e.fromAccountId, e.toAccountId, e.amount, e.unitFee], ['transfer', ctx.eigen, node, 500_000, 200]);
|
||||
assert.equal(eventsNetByAccount(ctx.eigen, ctx.btc), -500_200);
|
||||
assert.equal(eventsNetByAccount(node, ctx.btc), 500_000);
|
||||
});
|
||||
|
||||
it('weigert een gewone beweging, die heeft een tegenpartij', async () => {
|
||||
beweging({ direction: 'out' });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await assert.rejects(() => resolveProposalAsInternal(voorstellen()[0].id), (e) => e.key === 'home.recNotInternal');
|
||||
assert.deepEqual(events(), []);
|
||||
assert.equal(voorstellen()[0].status, 'pending');
|
||||
});
|
||||
|
||||
it('de lijst draagt de soort en de fee mee', async () => {
|
||||
const wtx = beweging({ direction: 'out', feeSats: 12 });
|
||||
await voorstelVoor(wtx, 'channel');
|
||||
const p = listPendingProposals()[0];
|
||||
assert.equal(p.tx_kind, 'channel');
|
||||
assert.equal(p.tx_fee_sats, 12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveProposalAsInterest', () => {
|
||||
function forward(txid = 'fwd:1:2:3') {
|
||||
harness.exports.run(`INSERT INTO WalletSource (accountId, kind, value, createdAt) VALUES (?, 'nwc', 'x', '2026-01-01')`, [ctx.eigen]);
|
||||
harness.exports.run(`INSERT INTO WalletTransaction (accountId, sourceId, layer, txid, direction, valueSats, blockTime, createdAt)
|
||||
VALUES (?, ?, 'lightning', ?, 'in', 7, '2026-10-19T10:00:00Z', '2026-10-19')`, [ctx.eigen, harness.exports.lastId(), txid]);
|
||||
return harness.exports.lastId();
|
||||
}
|
||||
|
||||
it('boekt een forward als rente op het account, en terugdraaien haalt hem weg', async () => {
|
||||
const wtx = forward();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const id = voorstellen().find(p => p.walletTransactionId === wtx).id;
|
||||
const eventId = await resolveProposalAsInterest(id);
|
||||
const e = events().find(x => x.id === eventId);
|
||||
assert.deepEqual([e.type, e.amount, e.toAccountId, e.fromAccountId, e.fiatPrice], ['interest', 7, ctx.eigen, null, null]);
|
||||
assert.equal(e.occurredAt, '2026-10-19T10:00:00Z');
|
||||
assert.equal(voorstellen()[0].status, 'accepted_interest');
|
||||
await revertProposal(id);
|
||||
assert.deepEqual(events(), []);
|
||||
assert.equal(voorstellen()[0].status, 'pending');
|
||||
});
|
||||
|
||||
it('weigert een gewone inkomende betaling', async () => {
|
||||
const wtx = forward('hash-gewoon');
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const id = voorstellen().find(p => p.walletTransactionId === wtx).id;
|
||||
await assert.rejects(() => resolveProposalAsInterest(id), (e) => e.key === 'home.recNotForward');
|
||||
assert.deepEqual(events(), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('negeren en verwijderen', () => {
|
||||
it('ignoreProposal zet één voorstel op ignored', async () => {
|
||||
beweging();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
await ignoreProposal(voorstellen()[0].id);
|
||||
assert.equal(voorstellen()[0].status, 'ignored');
|
||||
});
|
||||
|
||||
it('ignoreProposal weigert een al afgehandeld voorstel', async () => {
|
||||
beweging();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const id = voorstellen()[0].id;
|
||||
await ignoreProposal(id);
|
||||
await assert.rejects(() => ignoreProposal(id), (e) => e.key === 'home.recHandled_ignored');
|
||||
});
|
||||
|
||||
it('ignoreAllPendingProposals negeert er meerdere en meldt het aantal', async () => {
|
||||
beweging({ txid: 'a' });
|
||||
beweging({ txid: 'b' });
|
||||
beweging({ txid: 'c' });
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
assert.equal(await ignoreAllPendingProposals(), 3);
|
||||
assert.ok(voorstellen().every(p => p.status === 'ignored'));
|
||||
});
|
||||
|
||||
it('ignoreAllPendingProposals schrijft niet als er niets openstaat', async () => {
|
||||
const voor = harness.calls.persist;
|
||||
assert.equal(await ignoreAllPendingProposals(), 0);
|
||||
assert.equal(harness.calls.persist, voor, 'geen wijziging, geen schrijfactie');
|
||||
});
|
||||
|
||||
it('deleteProposal laat het aangemaakte event en de koppeling staan', async () => {
|
||||
beweging();
|
||||
await syncTxProposals(ctx.eigen, ctx.btc);
|
||||
const id = voorstellen()[0].id;
|
||||
const eventId = await resolveProposalAsTrade(id, { fiatPrice: 1, fiatFee: null, occurredAt: null });
|
||||
|
||||
await deleteProposal(id);
|
||||
assert.deepEqual(voorstellen(), []);
|
||||
assert.equal(events().length, 1);
|
||||
assert.equal(events()[0].id, eventId);
|
||||
assert.equal(harness.exports.all(`SELECT * FROM WalletTransactionEventLink`).length, 1);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user