diff --git a/README.md b/README.md index bc01e17b..7e6ca255 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Lightning.Pub -![Lightning.Pub](https://github.com/shocknet/Lightning.Pub/raw/mac-install/pub_logo.png) +![Lightning.Pub](https://github.com/shocknet/Lightning.Pub/raw/master/pub_logo.png) ![GitHub last commit](https://img.shields.io/github/last-commit/shocknet/Lightning.Pub?style=flat-square) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) diff --git a/env.example b/env.example index 982db75e..43dee162 100644 --- a/env.example +++ b/env.example @@ -10,6 +10,8 @@ #LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon #LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log #BTC_NETWORK=mainnet +# 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 @@ -17,8 +19,7 @@ # To disable this feature entirely overwrite the env with "null" # LIQUIDITY_PROVIDER_PUB=null # DISABLE_LIQUIDITY_PROVIDER=false -# USE_ONLY_LIQUIDITY_PROVIDER=false -PROVIDER_RELAY_URL= +# PROVIDER_RELAY_URL= #SWAPS # BOLTZ_HTTP_URL= diff --git a/package-lock.json b/package-lock.json index 1ed2efef..4c0a372e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,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", @@ -4682,6 +4683,27 @@ "safe-buffer": "^5.1.1" } }, + "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", @@ -8193,4 +8215,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index f3004e0f..7db3acdb 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,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/scripts/install.sh b/scripts/install.sh index 42c5858a..479fb365 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -12,7 +12,7 @@ log() { echo "$message" | sed 's/\\e\[[0-9;]*m//g' >> "$TMP_LOG_FILE" } -SCRIPT_VERSION="0.3.0" +SCRIPT_VERSION="0.3.1" REPO="shocknet/Lightning.Pub" BRANCH="master" @@ -181,14 +181,14 @@ detect_os_arch # Define installation paths based on user if [ "$(id -u)" -eq 0 ]; then - IS_ROOT=true + export IS_ROOT=true # For root, install under /opt for system-wide access export INSTALL_DIR="/opt/lightning_pub" export UNIT_DIR="/etc/systemd/system" export SYSTEMCTL_CMD="systemctl" log "Running as root: App will be installed in $INSTALL_DIR" else - IS_ROOT=false + export IS_ROOT=false export INSTALL_DIR="$HOME/lightning_pub" export UNIT_DIR="$HOME/.config/systemd/user" export SYSTEMCTL_CMD="systemctl --user" diff --git a/scripts/start_services.sh b/scripts/start_services.sh index e8f16959..c2c46800 100755 --- a/scripts/start_services.sh +++ b/scripts/start_services.sh @@ -15,6 +15,12 @@ start_services() { if [ "$OS" = "Linux" ]; then if [ "$SYSTEMCTL_AVAILABLE" = true ]; then + # Enable linger for user services so they persist after logout + if [ "$IS_ROOT" = false ] && command -v loginctl &> /dev/null; then + log "Enabling linger for user services to persist after logout..." + loginctl enable-linger || log "Warning: Failed to enable linger. Services may stop after logout." + fi + mkdir -p "$UNIT_DIR" # Check and create lnd.service if needed (only if it doesn't exist) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 5f71285c..323cda3d 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,12 +386,40 @@ 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 } } 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 @@ -354,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.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } @@ -409,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") @@ -486,6 +569,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 @@ -502,17 +588,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 @@ -531,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/applicationManager.ts b/src/services/main/applicationManager.ts index 6d8a30d2..6f671457 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 06656506..fb80051c 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 { NostrSend } from '../nostr/nostrPool.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" @@ -206,6 +205,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) { @@ -284,6 +288,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) { @@ -426,6 +438,33 @@ export default class { this.utils.nostrSender.Send({ 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 1e27489e..006b8e5d 100644 --- a/src/services/main/init.ts +++ b/src/services/main/init.ts @@ -42,7 +42,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 8e4d635c..8715ac7b 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -74,6 +74,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) { @@ -102,6 +106,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.GetMaxWithdrawable() const { remote } = await this.lnd.ChannelBalance() const drainable = Math.min(maxW, remote) @@ -159,6 +167,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 2ddee539..70400a20 100644 --- a/src/services/main/offerManager.ts +++ b/src/services/main/offerManager.ts @@ -151,7 +151,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 @@ -185,7 +191,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 } @@ -194,17 +200,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) { @@ -237,20 +243,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 e24efd48..976d0f0f 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -123,6 +123,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/unlocker.ts b/src/services/main/unlocker.ts index f129834f..230918f7 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) @@ -211,11 +216,10 @@ export class Unlocker { throw new Error("node pub not found") } const encrypted = await this.storage.liquidityStorage.GetNoodeSeed(this.nodePub) - if (!encrypted || !encrypted.seed) { + if (!encrypted) { throw new Error("seed not found") } - - const decrypted = this.DecryptWalletSeed(JSON.parse(encrypted.seed)) + const decrypted = this.DecryptWalletSeed(JSON.parse(encrypted)) return { seed: decrypted } } 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/liquidityStorage.ts b/src/services/storage/liquidityStorage.ts index 0a609ed5..74364e33 100644 --- a/src/services/storage/liquidityStorage.ts +++ b/src/services/storage/liquidityStorage.ts @@ -18,7 +18,8 @@ export class LiquidityStorage { } async GetNoodeSeed(pubkey: string) { - return this.dbs.FindOne('LndNodeInfo', { where: { pubkey, seed: Not(IsNull()) } }) + const node = await this.dbs.FindOne('LndNodeInfo', { where: { pubkey/* , seed: Not(IsNull()) */ } }) + return node?.seed } async SaveNodeSeed(pubkey: string, seed: string) { 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 56c1eefd..58ada956 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -29,13 +29,14 @@ import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_a import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js' import { TxSwap1762890527098 } from './1762890527098-tx_swap.js' import { TxSwapAddress1764779178945 } from './1764779178945-tx_swap_address.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, TxSwap1762890527098, TxSwapAddress1764779178945] + UserAccess1759426050669, AddBlindToUserOffer1760000000000, ApplicationAvatarUrl1761000001000, AdminSettings1761683639419, TxSwap1762890527098, TxSwapAddress1764779178945, 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 47205463..d9bfdfc2 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -15,7 +15,7 @@ import TransactionsQueue from "./db/transactionsQueue.js"; import { LoggedEvent } from './eventsLog.js'; import { StorageInterface } from './db/storageInterface.js'; import { TransactionSwap } from './entity/TransactionSwap.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 @@ -130,7 +130,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) }