Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// FinancialDataLog entries created on 2026-06-16 carry a totalBalanceChf that is
// implausibly high (> 50 000 CHF) compared to the expected operating-equity range,
// yet they were stamped valid=true. This is a recurrence of the same transient
// accounting spike handled for 2026-06-15 (see InvalidateHighTotalBalanceLogs
// 1781527084203): plusBalanceChf jumped ahead of the corresponding minusBalanceChf
// booking, so deltas at the elevated baseline stayed within
// financeLogTotalBalanceChangeLimit and the invalid flag was never set. This
// migration marks those entries invalid so monitoring dashboards and anomaly
// alerts reflect the period correctly.
//
// Threshold : totalBalanceChf > 50 000 (well above the normal ~20–30 k band)
// Scope : entries created on 2026-06-16 (UTC), no upper bound so every
// affected row of the day is covered regardless of sub-second timing.
//
// Env-guarded: the COUNT pre-check makes up() a no-op where no rows match
// (staging/dev). down() re-stamps valid=true for the same window/threshold; it
// cannot distinguish rows already invalid before up() ran, so it may over-restore
// a small number of entries — accepted for this one-shot fix.
module.exports = class InvalidateHighTotalBalanceLogs1781598468039 {
name = 'InvalidateHighTotalBalanceLogs1781598468039';

async up(queryRunner) {
const [{ count }] = await queryRunner.query(`
SELECT COUNT(*) AS count FROM log
WHERE subsystem = 'FinancialDataLog'
AND created >= '2026-06-16T00:00:00Z'
AND created < '2026-06-17T00:00:00Z'
AND (message::jsonb -> 'balancesTotal' ->> 'totalBalanceChf')::numeric > 50000
AND valid = true
`);
if (parseInt(count) === 0) return;

await queryRunner.query(`
UPDATE log SET valid = false
WHERE subsystem = 'FinancialDataLog'
AND created >= '2026-06-16T00:00:00Z'
AND created < '2026-06-17T00:00:00Z'
AND (message::jsonb -> 'balancesTotal' ->> 'totalBalanceChf')::numeric > 50000
AND valid = true
`);
}

async down(queryRunner) {
const [{ count }] = await queryRunner.query(`
SELECT COUNT(*) AS count FROM log
WHERE subsystem = 'FinancialDataLog'
AND created >= '2026-06-16T00:00:00Z'
AND created < '2026-06-17T00:00:00Z'
AND (message::jsonb -> 'balancesTotal' ->> 'totalBalanceChf')::numeric > 50000
AND valid = false
`);
if (parseInt(count) === 0) return;

await queryRunner.query(`
UPDATE log SET valid = true
WHERE subsystem = 'FinancialDataLog'
AND created >= '2026-06-16T00:00:00Z'
AND created < '2026-06-17T00:00:00Z'
AND (message::jsonb -> 'balancesTotal' ->> 'totalBalanceChf')::numeric > 50000
AND valid = false
`);
}
};
21 changes: 1 addition & 20 deletions src/integration/lightning/lightning-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { BadRequestException } from '@nestjs/common';
import { randomBytes } from 'crypto';
import { Agent } from 'https';
import { Config } from 'src/config/config';
Expand Down Expand Up @@ -200,15 +199,11 @@ export class LightningClient implements CoinOnly {

// --- LNURLp REWRITE --- //
async getLnurlpPaymentRequest(linkId: string): Promise<LnurlPayRequestDto> {
this.validateLinkId(linkId);

const lnBitsUrl = `${Config.blockchain.lightning.lnbits.lnurlpUrl}/${linkId}`;
return this.http.get(lnBitsUrl, this.httpLnBitsConfig());
}

async getLnurlpInvoice(linkId: string, params: any): Promise<LnurlpInvoiceDto> {
this.validateLinkId(linkId);

const lnBitsCallbackUrl = `${Config.blockchain.lightning.lnbits.lnurlpApiUrl}/lnurl/cb/${linkId}`;
return this.http.get<LnurlpInvoiceDto>(lnBitsCallbackUrl, this.httpLnBitsConfig(params));
}
Expand All @@ -222,8 +217,6 @@ export class LightningClient implements CoinOnly {
}

async getLnurlpLink(linkId: string): Promise<LnurlpLinkDto> {
this.validateLinkId(linkId);

return this.http.get<LnurlpLinkDto>(
`${Config.blockchain.lightning.lnbits.lnurlpApiUrl}/links/${linkId}`,
this.httpLnBitsConfig(),
Expand Down Expand Up @@ -254,8 +247,6 @@ export class LightningClient implements CoinOnly {
}

async updateLnurlpLink(linkId: string, data: LnurlpLinkUpdateDto): Promise<LnurlpLinkDto> {
this.validateLinkId(linkId);

return this.http.put<LnurlpLinkDto>(
`${Config.blockchain.lightning.lnbits.lnurlpApiUrl}/links/${linkId}`,
data,
Expand Down Expand Up @@ -298,8 +289,6 @@ export class LightningClient implements CoinOnly {
}

async getLnurlwLink(linkId: string): Promise<LnurlwLinkDto> {
this.validateLinkId(linkId);

return this.http.get<LnurlwLinkDto>(
`${Config.blockchain.lightning.lnbits.lnurlwApiUrl}/links/${linkId}`,
this.httpLnBitsConfig(),
Expand Down Expand Up @@ -339,16 +328,11 @@ export class LightningClient implements CoinOnly {
// --- LNURLd --- //

async getLnurlDevice(id: string, params: any): Promise<LnurlWithdrawRequestDto> {
this.validateLinkId(id);

const url = `${this.getDeviceUrl()}/${id}`;
return this.http.get(url, this.httpLnBitsConfig(params));
}

async getLnurlDeviceCallback(id: string, variable: string, params: any): Promise<LnurlwInvoiceDto> {
this.validateLinkId(id);
this.validateLinkId(variable);

const url = `${this.getDeviceUrl()}/cb/${id}/${variable}`;
return this.http.get(url, this.httpLnBitsConfig(params));
}
Expand All @@ -359,13 +343,10 @@ export class LightningClient implements CoinOnly {
}

// --- HELPER METHODS --- //
private validateLinkId(linkId: string): void {
if (!/^[\w-]+$/.test(linkId)) throw new BadRequestException('Invalid link id');
}

private httpLnBitsConfig(params?: any): HttpRequestConfig {
return {
httpsAgent: this.tlsAgent,
headers: { 'X-Forwarded-Proto': 'https', Host: new URL(Config.url()).hostname },
params: { 'api-key': Config.blockchain.lightning.lnbits.apiKey, ...params },
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export class LnUrlForwardService {
id: string,
params: any,
): Promise<LnurlPayRequestDto | PaymentLinkPayRequestDto | LnurlpInvoiceDto | PaymentLinkEvmPaymentDto> {
this.validateLinkId(id);

if (id.startsWith(this.PAYMENT_LINK_PREFIX) || id.startsWith(this.PAYMENT_LINK_PAYMENT_PREFIX)) {
const payRequest = await this.paymentLinkService.createPayRequest(
id,
Expand Down Expand Up @@ -78,6 +80,8 @@ export class LnUrlForwardService {

// callback
async lnurlpCallbackForward(id: string, params: any): Promise<LnurlpInvoiceDto | PaymentLinkEvmPaymentDto> {
this.validateLinkId(id);

if (id.startsWith(this.PAYMENT_LINK_PREFIX) || id.startsWith(this.PAYMENT_LINK_PAYMENT_PREFIX)) {
const transferInfo = this.getPaymentTransferInfo(params);
return this.paymentLinkPaymentService.createActivationRequest(id, transferInfo);
Expand Down Expand Up @@ -138,6 +142,8 @@ export class LnUrlForwardService {

// --- LNURLw --- //
async lnurlwForward(id: string): Promise<LnurlWithdrawRequestDto> {
this.validateLinkId(id);

const withdrawRequest = await this.client.getLnurlwWithdrawRequest(id);

withdrawRequest.callback = LightningHelper.createLnurlwCallbackUrl(id);
Expand All @@ -146,11 +152,15 @@ export class LnUrlForwardService {
}

async lnurlwCallbackForward(id: string, params: any): Promise<LnurlwInvoiceDto> {
this.validateLinkId(id);

return this.client.sendLnurlwInvoice(id, params);
}

// --- LNURLd --- //
async lnurldForward(deviceId: string, params: any): Promise<LnurlWithdrawRequestDto> {
this.validateLinkId(deviceId);

const withdrawRequest = await this.client.getLnurlDevice(deviceId, params);

const [paymentId, variable] = withdrawRequest.callback.split('/').slice(-2);
Expand All @@ -160,6 +170,9 @@ export class LnUrlForwardService {
}

async lnurldCallbackForward(id: string, variable: string, params: any): Promise<LnurlwInvoiceDto> {
this.validateLinkId(id);
this.validateLinkId(variable);

return this.client.getLnurlDeviceCallback(id, variable, params);
}

Expand All @@ -172,4 +185,9 @@ export class LnUrlForwardService {

return this.paymentLinkService.createPayRequest(pendingPayment.uniqueId);
}

// --- HELPERS --- //
private validateLinkId(id: string): void {
if (!/^[\w-]+$/.test(id)) throw new BadRequestException('Invalid link id');
}
}
Loading