From e69512748deaa5b7e0cdbfc9ae281714a14179f9 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Fri, 12 Dec 2025 14:10:37 -0500 Subject: [PATCH 1/8] feat: noffer receipts --- src/services/main/applicationManager.ts | 7 ++-- src/services/main/index.ts | 41 +++++++++++++++++-- src/services/main/offerManager.ts | 24 +++++++---- .../storage/entity/UserReceivingInvoice.ts | 12 ++++++ .../1765354000000-clink_requester.ts | 15 +++++++ src/services/storage/paymentStorage.ts | 7 ++-- 6 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 src/services/storage/migrations/1765354000000-clink_requester.ts diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 76ae76db..14646746 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -7,7 +7,7 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js' import { PubLogger, getLogger } from '../helpers/logger.js' import crypto from 'crypto' import { Application } from '../storage/entity/Application.js' -import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js' +import { ZapInfo, ClinkRequester } from '../storage/entity/UserReceivingInvoice.js' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' import SettingsManager from './settingsManager.js' const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds @@ -185,7 +185,7 @@ export default class { return invoice } - async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest): Promise { + async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest, clinkRequester?: ClinkRequester): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const log = getLogger({ appName: app.name }) const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier) @@ -200,7 +200,8 @@ export default class { callbackUrl: cbUrl, expiry: expiry, expectedPayer: payer.user, linkedApplication: app, zapInfo, offerId: req.offer_string, payerData: req.payer_data?.data, rejectUnauthorized: req.rejectUnauthorized, token: req.token, - blind: req.invoice_req.blind + blind: req.invoice_req.blind, + clinkRequester } const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts) return { diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 00f65a68..41eb2e9b 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -10,11 +10,10 @@ import { AddressPaidCb, ChannelEventCb, HtlcCb, InvoicePaidCb, NewBlockCb } from import { ERROR, getLogger, PubLogger } from "../helpers/logger.js" import AppUserManager from "./appUserManager.js" import { Application } from '../storage/entity/Application.js' -import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js' +import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js' import { UnsignedEvent } from 'nostr-tools' -import { NostrEvent, NostrSend } from '../nostr/handler.js' +import { NostrSend } from '../nostr/handler.js' import MetricsManager from '../metrics/index.js' -import { LoggedEvent } from '../storage/eventsLog.js' import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityManager } from "./liquidityManager.js" import { Utils } from "../helpers/utilsWrapper.js" @@ -291,6 +290,14 @@ export default class { } catch (err: any) { log(ERROR, "cannot create zap receipt", err.message || "") } + // Send CLINK receipt if this invoice was from a noffer request + try { + if (userInvoice.clink_requester) { + this.createClinkReceipt(log, userInvoice) + } + } catch (err: any) { + log(ERROR, "cannot create clink receipt", err.message || "") + } this.liquidityManager.afterInInvoicePaid() this.utils.stateBundler.AddTxPoint('invoiceWasPaid', amount, { used, from: 'system', timeDiscount: true }, userInvoice.linkedApplication.app_id) } catch (err: any) { @@ -432,6 +439,34 @@ export default class { this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }, zapInfo.relays || undefined) } + async createClinkReceipt(log: PubLogger, invoice: UserReceivingInvoice) { + const clinkRequester = invoice.clink_requester + if (!clinkRequester || !invoice.linkedApplication) { + return + } + log("📤 [CLINK RECEIPT] Sending payment receipt", { + toPub: clinkRequester.pub, + eventId: clinkRequester.eventId + }) + // Receipt payload - payer's wallet already has the preimage + const content = JSON.stringify({ res: 'ok' }) + const event: UnsignedEvent = { + content, + created_at: Math.floor(Date.now() / 1000), + kind: 21001, + pubkey: "", + tags: [ + ["p", clinkRequester.pub], + ["e", clinkRequester.eventId], + ["clink_version", "1"] + ], + } + this.nostrSend( + { type: 'app', appId: clinkRequester.appId }, + { type: 'event', event, encrypt: { toPub: clinkRequester.pub } } + ) + } + async ResetNostr() { const apps = await this.storage.applicationStorage.GetApplications() const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] diff --git a/src/services/main/offerManager.ts b/src/services/main/offerManager.ts index bbc3b64d..9f3b92f3 100644 --- a/src/services/main/offerManager.ts +++ b/src/services/main/offerManager.ts @@ -10,6 +10,7 @@ import { UserOffer } from '../storage/entity/UserOffer.js'; import { LiquidityManager } from "./liquidityManager.js" import { NofferData, OfferPriceType, nofferEncode } from '@shocknet/clink-sdk'; import SettingsManager from "./settingsManager.js"; +import { ClinkRequester } from '../storage/entity/UserReceivingInvoice.js'; const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => { const offerStr = offer.offer_id @@ -164,7 +165,14 @@ export class OfferManager { payerData: offerReq.payer_data }) - const offerInvoice = await this.getNofferInvoice(offerReq, event.appId) + // Store requester info for sending receipt when invoice is paid + const clinkRequester: ClinkRequester = { + pub: event.pub, + eventId: event.id, + appId: event.appId + } + + const offerInvoice = await this.getNofferInvoice(offerReq, event.appId, clinkRequester) if (!offerInvoice.success) { const code = offerInvoice.code @@ -198,7 +206,7 @@ export class OfferManager { return } - async HandleDefaultUserOffer(offerReq: NofferData, appId: string, remote: number, { memo, expiry }: { memo?: string, expiry?: number }): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + async HandleDefaultUserOffer(offerReq: NofferData, appId: string, remote: number, { memo, expiry }: { memo?: string, expiry?: number }, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { const { amount_sats: amount, offer } = offerReq if (!amount || isNaN(amount) || amount < 10 || amount > remote) { return { success: false, code: 5, max: remote } @@ -207,17 +215,17 @@ export class OfferManager { http_callback_url: "", payer_identifier: offer, receiver_identifier: offer, invoice_req: { amountSats: amount, memo: memo || "Default CLINK Offer", zap: offerReq.zap, expiry }, offer_string: 'offer' - }) + }, clinkRequester) return { success: true, invoice: res.invoice } } - async HandleUserOffer(offerReq: NofferData, appId: string, remote: number): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + async HandleUserOffer(offerReq: NofferData, appId: string, remote: number, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { const { amount_sats: amount, offer } = offerReq const userOffer = await this.storage.offerStorage.GetOffer(offer) const expiry = offerReq.expires_in_seconds ? offerReq.expires_in_seconds : undefined if (!userOffer) { - return this.HandleDefaultUserOffer(offerReq, appId, remote, { memo: offerReq.description, expiry }) + return this.HandleDefaultUserOffer(offerReq, appId, remote, { memo: offerReq.description, expiry }, clinkRequester) } if (userOffer.app_user_id === userOffer.offer_id) { if (userOffer.price_sats !== 0 || userOffer.payer_data) { @@ -250,11 +258,11 @@ export class OfferManager { offer_string: offer, rejectUnauthorized: userOffer.rejectUnauthorized, token: userOffer.bearer_token - }) + }, clinkRequester) return { success: true, invoice: res.invoice } } - async getNofferInvoice(offerReq: NofferData, appId: string): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + async getNofferInvoice(offerReq: NofferData, appId: string, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { try { const { remote } = await this.lnd.ChannelBalance() let maxSendable = remote @@ -263,7 +271,7 @@ export class OfferManager { } const split = offerReq.offer.split(':') if (split.length === 1) { - return this.HandleUserOffer(offerReq, appId, maxSendable) + return this.HandleUserOffer(offerReq, appId, maxSendable, clinkRequester) } else if (split[0] === 'p') { const product = await this.productManager.NewProductInvoice(split[1]) return { success: true, invoice: product.invoice } diff --git a/src/services/storage/entity/UserReceivingInvoice.ts b/src/services/storage/entity/UserReceivingInvoice.ts index f8fb7001..3d6b7056 100644 --- a/src/services/storage/entity/UserReceivingInvoice.ts +++ b/src/services/storage/entity/UserReceivingInvoice.ts @@ -8,6 +8,12 @@ export type ZapInfo = { relays: string[] description: string } + +export type ClinkRequester = { + pub: string // requester's pubkey + eventId: string // original request event ID + appId: string // app context for nostrSend +} @Entity() @Index("recv_invoice_paid_serial", ["user.serial_id", "paid_at_unix", "serial_id"], { where: "paid_at_unix > 0" }) export class UserReceivingInvoice { @@ -80,6 +86,12 @@ export class UserReceivingInvoice { }) liquidityProvider?: string + @Column({ + nullable: true, + type: 'simple-json' + }) + clink_requester?: ClinkRequester + @CreateDateColumn() created_at: Date diff --git a/src/services/storage/migrations/1765354000000-clink_requester.ts b/src/services/storage/migrations/1765354000000-clink_requester.ts new file mode 100644 index 00000000..01ee2030 --- /dev/null +++ b/src/services/storage/migrations/1765354000000-clink_requester.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ClinkRequester1765354000000 implements MigrationInterface { + name = 'ClinkRequester1765354000000' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester" text`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester"`); + } + +} + diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index ef29957c..52a20613 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -3,7 +3,7 @@ import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, M import { User } from './entity/User.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; -import { UserReceivingInvoice, ZapInfo } from './entity/UserReceivingInvoice.js'; +import { UserReceivingInvoice, ZapInfo, ClinkRequester } from './entity/UserReceivingInvoice.js'; import { UserReceivingAddress } from './entity/UserReceivingAddress.js'; import { Product } from './entity/Product.js'; import UserStorage from './userStorage.js'; @@ -14,7 +14,7 @@ import { Application } from './entity/Application.js'; import TransactionsQueue from "./db/transactionsQueue.js"; import { LoggedEvent } from './eventsLog.js'; import { StorageInterface } from './db/storageInterface.js'; -export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record, rejectUnauthorized?: boolean, token?: string, blind?: boolean } +export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequester?: ClinkRequester } export const defaultInvoiceExpiry = 60 * 60 export default class { dbs: StorageInterface @@ -129,7 +129,8 @@ export default class { offer_id: options.offerId, payer_data: options.payerData, rejectUnauthorized: options.rejectUnauthorized, - bearer_token: options.token + bearer_token: options.token, + clink_requester: options.clinkRequester }, txId) } From 8e01b284c7c9813fc841c82d53d3d8e9a071b968 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Fri, 12 Dec 2025 14:23:28 -0500 Subject: [PATCH 2/8] register receipts --- src/services/storage/migrations/runner.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/storage/migrations/runner.ts b/src/services/storage/migrations/runner.ts index c110859c..d7a99ec7 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -27,13 +27,14 @@ import { UserAccess1759426050669 } from './1759426050669-user_access.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js' +import { ClinkRequester1765354000000 } from './1765354000000-clink_requester.js' export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, - UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419] + UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, ClinkRequester1765354000000] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => { From 1eb3a1f6ea096aa9d08b1c70a03383dd57e7fca4 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Fri, 12 Dec 2025 14:35:31 -0500 Subject: [PATCH 3/8] columns --- src/services/main/applicationManager.ts | 7 ++++--- src/services/main/index.ts | 17 ++++++++--------- src/services/main/offerManager.ts | 12 +++++------- .../storage/entity/UserReceivingInvoice.ts | 15 +++++---------- .../migrations/1765354000000-clink_requester.ts | 7 ++++--- src/services/storage/paymentStorage.ts | 7 ++++--- 6 files changed, 30 insertions(+), 35 deletions(-) diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 14646746..c874d36a 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -7,7 +7,7 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js' import { PubLogger, getLogger } from '../helpers/logger.js' import crypto from 'crypto' import { Application } from '../storage/entity/Application.js' -import { ZapInfo, ClinkRequester } from '../storage/entity/UserReceivingInvoice.js' +import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' import SettingsManager from './settingsManager.js' const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds @@ -185,7 +185,7 @@ export default class { return invoice } - async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest, clinkRequester?: ClinkRequester): Promise { + async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest, clinkRequester?: { pub: string, eventId: string }): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const log = getLogger({ appName: app.name }) const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier) @@ -201,7 +201,8 @@ export default class { offerId: req.offer_string, payerData: req.payer_data?.data, rejectUnauthorized: req.rejectUnauthorized, token: req.token, blind: req.invoice_req.blind, - clinkRequester + clinkRequesterPub: clinkRequester?.pub, + clinkRequesterEventId: clinkRequester?.eventId } const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts) return { diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 41eb2e9b..dea44a6f 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -292,7 +292,7 @@ export default class { } // Send CLINK receipt if this invoice was from a noffer request try { - if (userInvoice.clink_requester) { + if (userInvoice.clink_requester_pub && userInvoice.clink_requester_event_id) { this.createClinkReceipt(log, userInvoice) } } catch (err: any) { @@ -440,13 +440,12 @@ export default class { } async createClinkReceipt(log: PubLogger, invoice: UserReceivingInvoice) { - const clinkRequester = invoice.clink_requester - if (!clinkRequester || !invoice.linkedApplication) { + if (!invoice.clink_requester_pub || !invoice.clink_requester_event_id || !invoice.linkedApplication) { return } log("📤 [CLINK RECEIPT] Sending payment receipt", { - toPub: clinkRequester.pub, - eventId: clinkRequester.eventId + toPub: invoice.clink_requester_pub, + eventId: invoice.clink_requester_event_id }) // Receipt payload - payer's wallet already has the preimage const content = JSON.stringify({ res: 'ok' }) @@ -456,14 +455,14 @@ export default class { kind: 21001, pubkey: "", tags: [ - ["p", clinkRequester.pub], - ["e", clinkRequester.eventId], + ["p", invoice.clink_requester_pub], + ["e", invoice.clink_requester_event_id], ["clink_version", "1"] ], } this.nostrSend( - { type: 'app', appId: clinkRequester.appId }, - { type: 'event', event, encrypt: { toPub: clinkRequester.pub } } + { type: 'app', appId: invoice.linkedApplication.app_id }, + { type: 'event', event, encrypt: { toPub: invoice.clink_requester_pub } } ) } diff --git a/src/services/main/offerManager.ts b/src/services/main/offerManager.ts index 9f3b92f3..ec21c6f4 100644 --- a/src/services/main/offerManager.ts +++ b/src/services/main/offerManager.ts @@ -10,7 +10,6 @@ import { UserOffer } from '../storage/entity/UserOffer.js'; import { LiquidityManager } from "./liquidityManager.js" import { NofferData, OfferPriceType, nofferEncode } from '@shocknet/clink-sdk'; import SettingsManager from "./settingsManager.js"; -import { ClinkRequester } from '../storage/entity/UserReceivingInvoice.js'; const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => { const offerStr = offer.offer_id @@ -166,10 +165,9 @@ export class OfferManager { }) // Store requester info for sending receipt when invoice is paid - const clinkRequester: ClinkRequester = { + const clinkRequester = { pub: event.pub, - eventId: event.id, - appId: event.appId + eventId: event.id } const offerInvoice = await this.getNofferInvoice(offerReq, event.appId, clinkRequester) @@ -206,7 +204,7 @@ export class OfferManager { return } - async HandleDefaultUserOffer(offerReq: NofferData, appId: string, remote: number, { memo, expiry }: { memo?: string, expiry?: number }, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + async HandleDefaultUserOffer(offerReq: NofferData, appId: string, remote: number, { memo, expiry }: { memo?: string, expiry?: number }, clinkRequester?: { pub: string, eventId: string }): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { const { amount_sats: amount, offer } = offerReq if (!amount || isNaN(amount) || amount < 10 || amount > remote) { return { success: false, code: 5, max: remote } @@ -219,7 +217,7 @@ export class OfferManager { return { success: true, invoice: res.invoice } } - async HandleUserOffer(offerReq: NofferData, appId: string, remote: number, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + async HandleUserOffer(offerReq: NofferData, appId: string, remote: number, clinkRequester?: { pub: string, eventId: string }): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { const { amount_sats: amount, offer } = offerReq const userOffer = await this.storage.offerStorage.GetOffer(offer) const expiry = offerReq.expires_in_seconds ? offerReq.expires_in_seconds : undefined @@ -262,7 +260,7 @@ export class OfferManager { return { success: true, invoice: res.invoice } } - async getNofferInvoice(offerReq: NofferData, appId: string, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { + async getNofferInvoice(offerReq: NofferData, appId: string, clinkRequester?: { pub: string, eventId: string }): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { try { const { remote } = await this.lnd.ChannelBalance() let maxSendable = remote diff --git a/src/services/storage/entity/UserReceivingInvoice.ts b/src/services/storage/entity/UserReceivingInvoice.ts index 3d6b7056..186c91b4 100644 --- a/src/services/storage/entity/UserReceivingInvoice.ts +++ b/src/services/storage/entity/UserReceivingInvoice.ts @@ -9,11 +9,6 @@ export type ZapInfo = { description: string } -export type ClinkRequester = { - pub: string // requester's pubkey - eventId: string // original request event ID - appId: string // app context for nostrSend -} @Entity() @Index("recv_invoice_paid_serial", ["user.serial_id", "paid_at_unix", "serial_id"], { where: "paid_at_unix > 0" }) export class UserReceivingInvoice { @@ -86,11 +81,11 @@ export class UserReceivingInvoice { }) liquidityProvider?: string - @Column({ - nullable: true, - type: 'simple-json' - }) - clink_requester?: ClinkRequester + @Column({ nullable: true }) + clink_requester_pub?: string + + @Column({ nullable: true }) + clink_requester_event_id?: string @CreateDateColumn() created_at: Date diff --git a/src/services/storage/migrations/1765354000000-clink_requester.ts b/src/services/storage/migrations/1765354000000-clink_requester.ts index 01ee2030..3028dce9 100644 --- a/src/services/storage/migrations/1765354000000-clink_requester.ts +++ b/src/services/storage/migrations/1765354000000-clink_requester.ts @@ -4,12 +4,13 @@ export class ClinkRequester1765354000000 implements MigrationInterface { name = 'ClinkRequester1765354000000' public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester" text`); + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester_pub" varchar`); + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester_event_id" varchar`); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester"`); + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester_pub"`); + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester_event_id"`); } } - diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 52a20613..4b27775b 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -3,7 +3,7 @@ import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, M import { User } from './entity/User.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; -import { UserReceivingInvoice, ZapInfo, ClinkRequester } from './entity/UserReceivingInvoice.js'; +import { UserReceivingInvoice, ZapInfo } from './entity/UserReceivingInvoice.js'; import { UserReceivingAddress } from './entity/UserReceivingAddress.js'; import { Product } from './entity/Product.js'; import UserStorage from './userStorage.js'; @@ -14,7 +14,7 @@ import { Application } from './entity/Application.js'; import TransactionsQueue from "./db/transactionsQueue.js"; import { LoggedEvent } from './eventsLog.js'; import { StorageInterface } from './db/storageInterface.js'; -export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequester?: ClinkRequester } +export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string } export const defaultInvoiceExpiry = 60 * 60 export default class { dbs: StorageInterface @@ -130,7 +130,8 @@ export default class { payer_data: options.payerData, rejectUnauthorized: options.rejectUnauthorized, bearer_token: options.token, - clink_requester: options.clinkRequester + clink_requester_pub: options.clinkRequesterPub, + clink_requester_event_id: options.clinkRequesterEventId }, txId) } From 319073e6b9a228277f918763fb91e8f395fe145b Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Fri, 12 Dec 2025 14:45:37 -0500 Subject: [PATCH 4/8] skip lnd env --- env.example | 2 ++ src/services/main/settings.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/env.example b/env.example index 527657ae..09761f8c 100644 --- a/env.example +++ b/env.example @@ -9,6 +9,8 @@ #LND_CERT_PATH=~/.lnd/tls.cert #LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon #LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log +# Bypass LND entirely and daisychain off the bootstrap provider (testing only) +#USE_ONLY_LIQUIDITY_PROVIDER=false #BOOTSTRAP_PEER # A trusted peer that will hold a node-level account until channel automation becomes affordable diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index c9594ffe..f6c7dffe 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -172,7 +172,8 @@ export const LoadLiquiditySettingsFromEnv = (dbEnv: Record Date: Fri, 12 Dec 2025 15:00:49 -0500 Subject: [PATCH 5/8] app-only mode --- src/services/lnd/lnd.ts | 35 ++++++++++++++++++++++++++++++++++- src/services/main/unlocker.ts | 5 +++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index e64035f2..a734ce62 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -61,6 +61,34 @@ export default class { this.newBlockCb = newBlockCb this.htlcCb = htlcCb this.channelEventCb = channelEventCb + this.liquidProvider = liquidProvider + + // Skip LND client initialization if using only liquidity provider + if (liquidProvider.getSettings().useOnlyLiquidityProvider) { + this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization") + // Create minimal dummy clients - they won't be used but prevent null reference errors + // Use insecure credentials since LND won't be accessed + const { lndAddr } = this.getSettings().lndNodeSettings + const insecureCreds = credentials.createInsecure() + const dummyMacaroonCreds = credentials.createFromMetadataGenerator( + function (args: any, callback: any) { + let metadata = new Metadata(); + callback(null, metadata); + }, + ); + const dummyCombinedCreds = credentials.combineChannelCredentials( + insecureCreds, + dummyMacaroonCreds, + ); + const dummyTransport = new GrpcTransport({ host: lndAddr || '127.0.0.1:10009', channelCredentials: dummyCombinedCreds }) + this.lightning = new LightningClient(dummyTransport) + this.invoices = new InvoicesClient(dummyTransport) + this.router = new RouterClient(dummyTransport) + this.chainNotifier = new ChainNotifierClient(dummyTransport) + this.walletKit = new WalletKitClient(dummyTransport) + return + } + const { lndAddr, lndCertPath, lndMacaroonPath } = this.getSettings().lndNodeSettings const lndCert = fs.readFileSync(lndCertPath); const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex'); @@ -86,7 +114,6 @@ export default class { this.router = new RouterClient(transport) this.chainNotifier = new ChainNotifierClient(transport) this.walletKit = new WalletKitClient(transport) - this.liquidProvider = liquidProvider } LockOutgoingOperations(): void { @@ -105,6 +132,12 @@ export default class { } async Warmup() { + // Skip LND warmup if using only liquidity provider + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND warmup") + this.ready = true + return + } // console.log("Warming up LND") this.SubscribeAddressPaid() this.SubscribeInvoicePaid() diff --git a/src/services/main/unlocker.ts b/src/services/main/unlocker.ts index f129834f..880ae4d6 100644 --- a/src/services/main/unlocker.ts +++ b/src/services/main/unlocker.ts @@ -55,6 +55,11 @@ export class Unlocker { } Unlock = async (): Promise<'created' | 'unlocked' | 'noaction'> => { + // Skip LND unlock if using only liquidity provider + if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND unlock") + return 'noaction' + } const { lndCert, macaroon } = this.getCreds() if (macaroon === "") { const { ln, pub } = await this.InitFlow(lndCert) From aed90ae9296e9b12f14622689d5a0e977a7bfd64 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Fri, 12 Dec 2025 15:08:01 -0500 Subject: [PATCH 6/8] bypasses --- src/services/lnd/lnd.ts | 38 ++++++++++++++++++++++++----------- src/services/main/init.ts | 2 +- src/services/main/watchdog.ts | 6 ++++++ 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index a734ce62..dae955bd 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -67,20 +67,10 @@ export default class { if (liquidProvider.getSettings().useOnlyLiquidityProvider) { this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization") // Create minimal dummy clients - they won't be used but prevent null reference errors - // Use insecure credentials since LND won't be accessed + // Use insecure credentials directly (can't combine them) const { lndAddr } = this.getSettings().lndNodeSettings const insecureCreds = credentials.createInsecure() - const dummyMacaroonCreds = credentials.createFromMetadataGenerator( - function (args: any, callback: any) { - let metadata = new Metadata(); - callback(null, metadata); - }, - ); - const dummyCombinedCreds = credentials.combineChannelCredentials( - insecureCreds, - dummyMacaroonCreds, - ); - const dummyTransport = new GrpcTransport({ host: lndAddr || '127.0.0.1:10009', channelCredentials: dummyCombinedCreds }) + const dummyTransport = new GrpcTransport({ host: lndAddr || '127.0.0.1:10009', channelCredentials: insecureCreds }) this.lightning = new LightningClient(dummyTransport) this.invoices = new InvoicesClient(dummyTransport) this.router = new RouterClient(dummyTransport) @@ -163,6 +153,18 @@ export default class { } async GetInfo(): Promise { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + // Return dummy info when bypass is enabled + return { + identityPubkey: '', + alias: '', + syncedToChain: false, + syncedToGraph: false, + blockHeight: 0, + blockHash: '', + uris: [] + } + } // console.log("Getting info") const res = await this.lightning.getInfo({}, DeadLineMetadata()) return res.response @@ -384,6 +386,9 @@ export default class { } async ChannelBalance(): Promise<{ local: number, remote: number }> { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return { local: 0, remote: 0 } + } // console.log("Getting channel balance") const res = await this.lightning.channelBalance({}) const r = res.response @@ -527,6 +532,9 @@ export default class { } async GetBalance(): Promise { // TODO: remove this + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return { confirmedBalance: 0, unconfirmedBalance: 0, totalBalance: 0, channelsBalance: [] } + } // console.log("Getting balance") const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response @@ -549,11 +557,17 @@ export default class { } async GetAllPaidInvoices(max: number) { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return { invoices: [] } + } // console.log("Getting all paid invoices") const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) return res.response } async GetAllPayments(max: number) { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return { payments: [] } + } // console.log("Getting all payments") const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n }) return res.response diff --git a/src/services/main/init.ts b/src/services/main/init.ts index 01d5625a..8c20195c 100644 --- a/src/services/main/init.ts +++ b/src/services/main/init.ts @@ -40,7 +40,7 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM const mainHandler = new Main(settingsManager, storageManager, adminManager, utils, unlocker) adminManager.setLND(mainHandler.lnd) await mainHandler.lnd.Warmup() - if (!settingsManager.getSettings().serviceSettings.skipSanityCheck) { + if (!settingsManager.getSettings().serviceSettings.skipSanityCheck && !settingsManager.getSettings().liquiditySettings.useOnlyLiquidityProvider) { const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd) await sanityChecker.VerifyEventsLog() } diff --git a/src/services/main/watchdog.ts b/src/services/main/watchdog.ts index 97e5bc91..1e6415cc 100644 --- a/src/services/main/watchdog.ts +++ b/src/services/main/watchdog.ts @@ -54,6 +54,12 @@ export class Watchdog { } } StartWatching = async () => { + // Skip watchdog if using only liquidity provider + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping watchdog") + this.ready = true + return + } this.log("Starting watchdog") this.startedAtUnix = Math.floor(Date.now() / 1000) const info = await this.lnd.GetInfo() From 77280736538903805bc9d62691bbbe837de9f78e Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Fri, 12 Dec 2025 16:00:52 -0500 Subject: [PATCH 7/8] more bypassing --- package-lock.json | 22 +++++++++++++ package.json | 1 + src/services/lnd/lnd.ts | 47 +++++++++++++++++++++++++-- src/services/main/index.ts | 5 +++ src/services/main/liquidityManager.ts | 12 +++++++ src/services/main/offerManager.ts | 16 ++++++--- src/services/main/paymentManager.ts | 5 +++ src/services/main/watchdog.ts | 4 +++ 8 files changed, 106 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ed242466..f9ad7caf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "globby": "^13.1.2", "grpc-tools": "^1.12.4", "jsonwebtoken": "^9.0.2", + "light-bolt11-decoder": "^3.2.0", "lodash": "^4.17.21", "nostr-tools": "^2.13.0", "pg": "^8.4.0", @@ -4333,6 +4334,27 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/light-bolt11-decoder": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz", + "integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==", + "license": "MIT", + "dependencies": { + "@scure/base": "1.1.1" + } + }, + "node_modules/light-bolt11-decoder/node_modules/@scure/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", + "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT" + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", diff --git a/package.json b/package.json index 123cd17c..56a45e1e 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "globby": "^13.1.2", "grpc-tools": "^1.12.4", "jsonwebtoken": "^9.0.2", + "light-bolt11-decoder": "^3.2.0", "lodash": "^4.17.21", "nostr-tools": "^2.13.0", "pg": "^8.4.0", diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index dae955bd..f6c72459 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -3,6 +3,7 @@ import crypto from 'crypto' import { credentials, Metadata } from '@grpc/grpc-js' import { GrpcTransport } from "@protobuf-ts/grpc-transport"; import fs from 'fs' +import { decode as decodeBolt11 } from 'light-bolt11-decoder' import * as Types from '../../../proto/autogenerated/ts/types.js' import { LightningClient } from '../../../proto/lnd/lightning.client.js' import { InvoicesClient } from '../../../proto/lnd/invoices.client.js' @@ -170,6 +171,9 @@ export default class { return res.response } async ListPendingChannels(): Promise { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return { pendingOpenChannels: [], pendingClosingChannels: [], pendingForceClosingChannels: [], waitingCloseChannels: [], totalLimboBalance: 0n } + } // console.log("Listing pending channels") const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata()) return res.response @@ -195,6 +199,10 @@ export default class { } async Health(): Promise { + // Skip health check when bypass is enabled + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return + } // console.log("Checking health") if (!this.ready) { throw new Error("not ready") @@ -324,6 +332,10 @@ export default class { } async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise { + // Force use of provider when bypass is enabled (addresses not supported by provider, but we should fail gracefully) + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + throw new Error("Address generation not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled") + } // console.log("Creating new address") let lndAddressType: AddressType switch (addressType) { @@ -355,7 +367,9 @@ export default class { async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise { // console.log("Creating new invoice") - if (useProvider) { + // Force use of provider when bypass is enabled + const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider + if (mustUseProvider) { console.log("using provider") const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) const providerDst = this.liquidProvider.GetProviderDestination() @@ -372,6 +386,23 @@ export default class { } async DecodeInvoice(paymentRequest: string): Promise { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + // Use light-bolt11-decoder when LND is bypassed + const decoded = decodeBolt11(paymentRequest) + let numSatoshis = 0 + let paymentHash = '' + + for (const section of decoded.sections) { + if (section.name === 'amount') { + // Amount is in millisatoshis + numSatoshis = Math.floor(Number(section.value) / 1000) + } else if (section.name === 'payment_hash') { + paymentHash = section.value as string + } + } + + return { numSatoshis, paymentHash } + } // console.log("Decoding invoice") const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } @@ -400,7 +431,9 @@ export default class { this.log("outgoing ops locked, rejecting payment request") throw new Error("lnd node is currently out of sync") } - if (useProvider) { + // Force use of provider when bypass is enabled + const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider + if (mustUseProvider) { const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from) const providerDst = this.liquidProvider.GetProviderDestination() return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } @@ -455,6 +488,10 @@ export default class { } async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise { + // Address payments not supported when bypass is enabled + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + throw new Error("Address payments not supported when USE_ONLY_LIQUIDITY_PROVIDER is enabled") + } // console.log("Paying address") if (this.outgoingOpsLocked) { this.log("outgoing ops locked, rejecting payment request") @@ -551,6 +588,9 @@ export default class { } async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return { forwardingEvents: [], lastOffsetIndex: indexOffset } + } // console.log("Getting forwarding history") const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata()) return response @@ -586,6 +626,9 @@ export default class { } async GetLatestPaymentIndex(from = 0) { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return from + } // console.log("Getting latest payment index") let indexOffset = BigInt(from) while (true) { diff --git a/src/services/main/index.ts b/src/services/main/index.ts index dea44a6f..601ff291 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -212,6 +212,11 @@ export default class { addressPaidCb: AddressPaidCb = (txOutput, address, amount, used) => { return this.storage.StartTransaction(async tx => { + // On-chain payments not supported when bypass is enabled + if (this.liquidityProvider.getSettings().useOnlyLiquidityProvider) { + getLogger({})("addressPaidCb called but USE_ONLY_LIQUIDITY_PROVIDER is enabled, ignoring") + return + } const { blockHeight } = await this.lnd.GetInfo() const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx) if (!userAddress) { diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index ac72975e..183988ef 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -71,6 +71,10 @@ export class LiquidityManager { } afterInInvoicePaid = async () => { + // Skip channel ordering if using only liquidity provider + if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + return + } try { await this.orderChannelIfNeeded() } catch (e: any) { @@ -91,6 +95,10 @@ export class LiquidityManager { afterOutInvoicePaid = async () => { } shouldDrainProvider = async () => { + // Skip draining when bypass is enabled + if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + return + } const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() const { remote } = await this.lnd.ChannelBalance() const drainable = Math.min(maxW, remote) @@ -148,6 +156,10 @@ export class LiquidityManager { shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => { + // Skip channel operations if using only liquidity provider + if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + return { shouldOpen: false } + } const threshold = this.settings.getSettings().lspSettings.channelThreshold if (threshold === 0) { return { shouldOpen: false } diff --git a/src/services/main/offerManager.ts b/src/services/main/offerManager.ts index ec21c6f4..11113746 100644 --- a/src/services/main/offerManager.ts +++ b/src/services/main/offerManager.ts @@ -262,10 +262,18 @@ export class OfferManager { async getNofferInvoice(offerReq: NofferData, appId: string, clinkRequester?: { pub: string, eventId: string }): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { try { - const { remote } = await this.lnd.ChannelBalance() - let maxSendable = remote - if (remote === 0 && (await this.liquidityManager.liquidityProvider.IsReady())) { - maxSendable = 10_000_000 + // When bypass is enabled, use provider balance instead of LND channel balance + let maxSendable = 0 + if (this.liquidityManager.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + if (await this.liquidityManager.liquidityProvider.IsReady()) { + maxSendable = 10_000_000 + } + } else { + const { remote } = await this.lnd.ChannelBalance() + maxSendable = remote + if (remote === 0 && (await this.liquidityManager.liquidityProvider.IsReady())) { + maxSendable = 10_000_000 + } } const split = offerReq.offer.split(':') if (split.length === 1) { diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 4dcd0b6e..08bf8ee0 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -115,6 +115,11 @@ export default class { } checkPendingLndPayment = async (log: PubLogger, p: UserInvoicePayment) => { + // Skip LND payment checks when bypass is enabled + if (this.liquidityManager.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND payment check for", p.serial_id) + return + } const decoded = await this.lnd.DecodeInvoice(p.invoice) const payment = await this.lnd.GetPaymentFromHash(decoded.paymentHash) if (!payment || payment.paymentHash !== decoded.paymentHash) { diff --git a/src/services/main/watchdog.ts b/src/services/main/watchdog.ts index 1e6415cc..778e09ce 100644 --- a/src/services/main/watchdog.ts +++ b/src/services/main/watchdog.ts @@ -211,6 +211,10 @@ export class Watchdog { } PaymentRequested = async () => { + // Skip watchdog check when bypass is enabled + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + return + } if (!this.ready) { throw new Error("Watchdog not ready") } From 7117cf8045e9f124b55f9cac7ee6a6d5db6d097b Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Sat, 13 Dec 2025 10:10:36 -0500 Subject: [PATCH 8/8] nits --- src/services/lnd/lnd.ts | 32 ++++++++++++------- src/services/main/index.ts | 2 +- .../storage/entity/UserReceivingInvoice.ts | 1 - .../1765354000000-clink_requester.ts | 16 ---------- .../1765497600000-clink_requester.ts | 25 +++++++++++++++ src/services/storage/migrations/runner.ts | 4 +-- 6 files changed, 48 insertions(+), 32 deletions(-) delete mode 100644 src/services/storage/migrations/1765354000000-clink_requester.ts create mode 100644 src/services/storage/migrations/1765497600000-clink_requester.ts diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index f6c72459..7ba9758a 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -388,20 +388,28 @@ export default class { async DecodeInvoice(paymentRequest: string): Promise { if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { // Use light-bolt11-decoder when LND is bypassed - const decoded = decodeBolt11(paymentRequest) - let numSatoshis = 0 - let paymentHash = '' - - for (const section of decoded.sections) { - if (section.name === 'amount') { - // Amount is in millisatoshis - numSatoshis = Math.floor(Number(section.value) / 1000) - } else if (section.name === 'payment_hash') { - paymentHash = section.value as string + try { + const decoded = decodeBolt11(paymentRequest) + let numSatoshis = 0 + let paymentHash = '' + + for (const section of decoded.sections) { + if (section.name === 'amount') { + // Amount is in millisatoshis + numSatoshis = Math.floor(Number(section.value) / 1000) + } else if (section.name === 'payment_hash') { + paymentHash = section.value as string + } } + + if (!paymentHash) { + throw new Error("Payment hash not found in invoice") + } + + return { numSatoshis, paymentHash } + } catch (err: any) { + throw new Error(`Failed to decode invoice: ${err.message}`) } - - return { numSatoshis, paymentHash } } // console.log("Decoding invoice") const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 601ff291..fcbb3cf8 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -298,7 +298,7 @@ export default class { // Send CLINK receipt if this invoice was from a noffer request try { if (userInvoice.clink_requester_pub && userInvoice.clink_requester_event_id) { - this.createClinkReceipt(log, userInvoice) + await this.createClinkReceipt(log, userInvoice) } } catch (err: any) { log(ERROR, "cannot create clink receipt", err.message || "") diff --git a/src/services/storage/entity/UserReceivingInvoice.ts b/src/services/storage/entity/UserReceivingInvoice.ts index 186c91b4..f0b69530 100644 --- a/src/services/storage/entity/UserReceivingInvoice.ts +++ b/src/services/storage/entity/UserReceivingInvoice.ts @@ -8,7 +8,6 @@ export type ZapInfo = { relays: string[] description: string } - @Entity() @Index("recv_invoice_paid_serial", ["user.serial_id", "paid_at_unix", "serial_id"], { where: "paid_at_unix > 0" }) export class UserReceivingInvoice { diff --git a/src/services/storage/migrations/1765354000000-clink_requester.ts b/src/services/storage/migrations/1765354000000-clink_requester.ts deleted file mode 100644 index 3028dce9..00000000 --- a/src/services/storage/migrations/1765354000000-clink_requester.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class ClinkRequester1765354000000 implements MigrationInterface { - name = 'ClinkRequester1765354000000' - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester_pub" varchar`); - await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester_event_id" varchar`); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester_pub"`); - await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester_event_id"`); - } - -} diff --git a/src/services/storage/migrations/1765497600000-clink_requester.ts b/src/services/storage/migrations/1765497600000-clink_requester.ts new file mode 100644 index 00000000..5568d4e3 --- /dev/null +++ b/src/services/storage/migrations/1765497600000-clink_requester.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ClinkRequester1765497600000 implements MigrationInterface { + name = 'ClinkRequester1765497600000' + + public async up(queryRunner: QueryRunner): Promise { + // Check if columns already exist (idempotent migration for existing databases) + const tableInfo = await queryRunner.query(`PRAGMA table_info("user_receiving_invoice")`); + const hasPubColumn = tableInfo.some((col: any) => col.name === 'clink_requester_pub'); + const hasEventIdColumn = tableInfo.some((col: any) => col.name === 'clink_requester_event_id'); + + if (!hasPubColumn) { + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester_pub" varchar(64)`); + } + if (!hasEventIdColumn) { + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester_event_id" varchar(64)`); + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester_pub"`); + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester_event_id"`); + } + +} diff --git a/src/services/storage/migrations/runner.ts b/src/services/storage/migrations/runner.ts index d7a99ec7..c87adf78 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -27,14 +27,14 @@ import { UserAccess1759426050669 } from './1759426050669-user_access.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js' -import { ClinkRequester1765354000000 } from './1765354000000-clink_requester.js' +import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js' export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, - UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, ClinkRequester1765354000000] + UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, ClinkRequester1765497600000] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => {