Merge branch 'master' into swaps-test

This commit is contained in:
boufni95 2025-12-17 18:13:46 +00:00
commit 4374d3f443
20 changed files with 283 additions and 34 deletions

View file

@ -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)

View file

@ -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=

24
package-lock.json generated
View file

@ -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 @@
}
}
}
}
}

View file

@ -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",

View file

@ -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"

View file

@ -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)

View file

@ -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<NodeInfo> {
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<PendingChannelsResponse> {
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<void> {
// 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<NewAddressResponse> {
// 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<Invoice> {
// 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<DecodedInvoice> {
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<SendCoinsResponse> {
// 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<BalanceInfo> { // 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<ForwardingHistoryResponse> {
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) {

View file

@ -185,7 +185,7 @@ export default class {
return invoice
}
async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest): Promise<Types.NewInvoiceResponse> {
async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest, clinkRequester?: { pub: string, eventId: string }): Promise<Types.NewInvoiceResponse> {
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 {

View file

@ -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]

View file

@ -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()
}

View file

@ -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 }

View file

@ -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 }

View file

@ -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) {

View file

@ -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 }
}

View file

@ -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")
}

View file

@ -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

View file

@ -18,7 +18,8 @@ export class LiquidityStorage {
}
async GetNoodeSeed(pubkey: string) {
return this.dbs.FindOne<LndNodeInfo>('LndNodeInfo', { where: { pubkey, seed: Not(IsNull()) } })
const node = await this.dbs.FindOne<LndNodeInfo>('LndNodeInfo', { where: { pubkey/* , seed: Not(IsNull()) */ } })
return node?.seed
}
async SaveNodeSeed(pubkey: string, seed: string) {

View file

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ClinkRequester1765497600000 implements MigrationInterface {
name = 'ClinkRequester1765497600000'
public async up(queryRunner: QueryRunner): Promise<void> {
// 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<void> {
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"`);
}
}

View file

@ -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<boolean> => {

View file

@ -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<string, string>, 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<string, string>, 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)
}