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/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 e64035f2..7ba9758a 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' @@ -61,6 +62,24 @@ 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 directly (can't combine them) + const { lndAddr } = this.getSettings().lndNodeSettings + const insecureCreds = credentials.createInsecure() + 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) + 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 +105,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 +123,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() @@ -130,11 +154,26 @@ 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 } 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 @@ -160,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") @@ -289,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) { @@ -320,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() @@ -337,6 +386,31 @@ export default class { } async DecodeInvoice(paymentRequest: string): Promise { + if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) { + // Use light-bolt11-decoder when LND is bypassed + 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}`) + } + } // console.log("Decoding invoice") const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } @@ -351,6 +425,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 @@ -362,7 +439,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 } @@ -417,6 +496,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") @@ -494,6 +577,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 @@ -510,17 +596,26 @@ 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 } 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 @@ -539,6 +634,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/applicationManager.ts b/src/services/main/applicationManager.ts index 76ae76db..c874d36a 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -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?: { 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) @@ -200,7 +200,9 @@ 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, + 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 00f65a68..fcbb3cf8 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" @@ -213,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) { @@ -291,6 +295,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_pub && userInvoice.clink_requester_event_id) { + await 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 +444,33 @@ export default class { this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event }, zapInfo.relays || undefined) } + async createClinkReceipt(log: PubLogger, invoice: UserReceivingInvoice) { + if (!invoice.clink_requester_pub || !invoice.clink_requester_event_id || !invoice.linkedApplication) { + return + } + log("📤 [CLINK RECEIPT] Sending payment receipt", { + 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' }) + const event: UnsignedEvent = { + content, + created_at: Math.floor(Date.now() / 1000), + kind: 21001, + pubkey: "", + tags: [ + ["p", invoice.clink_requester_pub], + ["e", invoice.clink_requester_event_id], + ["clink_version", "1"] + ], + } + this.nostrSend( + { type: 'app', appId: invoice.linkedApplication.app_id }, + { type: 'event', event, encrypt: { toPub: invoice.clink_requester_pub } } + ) + } + async ResetNostr() { const apps = await this.storage.applicationStorage.GetApplications() const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] 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/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 bbc3b64d..11113746 100644 --- a/src/services/main/offerManager.ts +++ b/src/services/main/offerManager.ts @@ -164,7 +164,13 @@ 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 = { + pub: event.pub, + eventId: event.id + } + + const offerInvoice = await this.getNofferInvoice(offerReq, event.appId, clinkRequester) if (!offerInvoice.success) { const code = offerInvoice.code @@ -198,7 +204,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?: { 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 } @@ -207,17 +213,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?: { 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 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,20 +256,28 @@ 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?: { 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) { - 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/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/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 => { + // 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) diff --git a/src/services/main/watchdog.ts b/src/services/main/watchdog.ts index 97e5bc91..778e09ce 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() @@ -205,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") } diff --git a/src/services/storage/entity/UserReceivingInvoice.ts b/src/services/storage/entity/UserReceivingInvoice.ts index f8fb7001..f0b69530 100644 --- a/src/services/storage/entity/UserReceivingInvoice.ts +++ b/src/services/storage/entity/UserReceivingInvoice.ts @@ -80,6 +80,12 @@ export class UserReceivingInvoice { }) liquidityProvider?: string + @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/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 c110859c..c87adf78 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 { 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] + 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 => { diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index ef29957c..4b27775b 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -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, clinkRequesterPub?: string, clinkRequesterEventId?: string } export const defaultInvoiceExpiry = 60 * 60 export default class { dbs: StorageInterface @@ -129,7 +129,9 @@ export default class { offer_id: options.offerId, payer_data: options.payerData, rejectUnauthorized: options.rejectUnauthorized, - bearer_token: options.token + bearer_token: options.token, + clink_requester_pub: options.clinkRequesterPub, + clink_requester_event_id: options.clinkRequesterEventId }, txId) }