Merge pull request #863 from shocknet/feat/generic-noffer-receipt

Feat/generic noffer receipt
This commit is contained in:
Justin (shocknet) 2025-12-16 15:57:41 -05:00 committed by GitHub
commit af6872229a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 270 additions and 25 deletions

View file

@ -9,6 +9,8 @@
#LND_CERT_PATH=~/.lnd/tls.cert #LND_CERT_PATH=~/.lnd/tls.cert
#LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon #LND_MACAROON_PATH=~/.lnd/data/chain/bitcoin/mainnet/admin.macaroon
#LND_LOG_DIR=~/.lnd/logs/bitcoin/mainnet/lnd.log #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 #BOOTSTRAP_PEER
# A trusted peer that will hold a node-level account until channel automation becomes affordable # A trusted peer that will hold a node-level account until channel automation becomes affordable

22
package-lock.json generated
View file

@ -33,6 +33,7 @@
"globby": "^13.1.2", "globby": "^13.1.2",
"grpc-tools": "^1.12.4", "grpc-tools": "^1.12.4",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"light-bolt11-decoder": "^3.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nostr-tools": "^2.13.0", "nostr-tools": "^2.13.0",
"pg": "^8.4.0", "pg": "^8.4.0",
@ -4333,6 +4334,27 @@
"safe-buffer": "~5.1.0" "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": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",

View file

@ -51,6 +51,7 @@
"globby": "^13.1.2", "globby": "^13.1.2",
"grpc-tools": "^1.12.4", "grpc-tools": "^1.12.4",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"light-bolt11-decoder": "^3.2.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nostr-tools": "^2.13.0", "nostr-tools": "^2.13.0",
"pg": "^8.4.0", "pg": "^8.4.0",

View file

@ -3,6 +3,7 @@ import crypto from 'crypto'
import { credentials, Metadata } from '@grpc/grpc-js' import { credentials, Metadata } from '@grpc/grpc-js'
import { GrpcTransport } from "@protobuf-ts/grpc-transport"; import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import fs from 'fs' import fs from 'fs'
import { decode as decodeBolt11 } from 'light-bolt11-decoder'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { LightningClient } from '../../../proto/lnd/lightning.client.js' import { LightningClient } from '../../../proto/lnd/lightning.client.js'
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js' import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
@ -61,6 +62,24 @@ export default class {
this.newBlockCb = newBlockCb this.newBlockCb = newBlockCb
this.htlcCb = htlcCb this.htlcCb = htlcCb
this.channelEventCb = channelEventCb 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 { lndAddr, lndCertPath, lndMacaroonPath } = this.getSettings().lndNodeSettings
const lndCert = fs.readFileSync(lndCertPath); const lndCert = fs.readFileSync(lndCertPath);
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex'); const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
@ -86,7 +105,6 @@ export default class {
this.router = new RouterClient(transport) this.router = new RouterClient(transport)
this.chainNotifier = new ChainNotifierClient(transport) this.chainNotifier = new ChainNotifierClient(transport)
this.walletKit = new WalletKitClient(transport) this.walletKit = new WalletKitClient(transport)
this.liquidProvider = liquidProvider
} }
LockOutgoingOperations(): void { LockOutgoingOperations(): void {
@ -105,6 +123,12 @@ export default class {
} }
async Warmup() { 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") // console.log("Warming up LND")
this.SubscribeAddressPaid() this.SubscribeAddressPaid()
this.SubscribeInvoicePaid() this.SubscribeInvoicePaid()
@ -130,11 +154,26 @@ export default class {
} }
async GetInfo(): Promise<NodeInfo> { 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") // console.log("Getting info")
const res = await this.lightning.getInfo({}, DeadLineMetadata()) const res = await this.lightning.getInfo({}, DeadLineMetadata())
return res.response return res.response
} }
async ListPendingChannels(): Promise<PendingChannelsResponse> { async ListPendingChannels(): Promise<PendingChannelsResponse> {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { pendingOpenChannels: [], pendingClosingChannels: [], pendingForceClosingChannels: [], waitingCloseChannels: [], totalLimboBalance: 0n }
}
// console.log("Listing pending channels") // console.log("Listing pending channels")
const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata()) const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata())
return res.response return res.response
@ -160,6 +199,10 @@ export default class {
} }
async Health(): Promise<void> { async Health(): Promise<void> {
// Skip health check when bypass is enabled
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return
}
// console.log("Checking health") // console.log("Checking health")
if (!this.ready) { if (!this.ready) {
throw new Error("not ready") throw new Error("not ready")
@ -289,6 +332,10 @@ export default class {
} }
async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise<NewAddressResponse> { 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") // console.log("Creating new address")
let lndAddressType: AddressType let lndAddressType: AddressType
switch (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> { async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise<Invoice> {
// console.log("Creating new 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") console.log("using provider")
const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry)
const providerDst = this.liquidProvider.GetProviderDestination() const providerDst = this.liquidProvider.GetProviderDestination()
@ -337,6 +386,31 @@ export default class {
} }
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> { 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") // console.log("Decoding invoice")
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash }
@ -351,6 +425,9 @@ export default class {
} }
async ChannelBalance(): Promise<{ local: number, remote: number }> { async ChannelBalance(): Promise<{ local: number, remote: number }> {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { local: 0, remote: 0 }
}
// console.log("Getting channel balance") // console.log("Getting channel balance")
const res = await this.lightning.channelBalance({}) const res = await this.lightning.channelBalance({})
const r = res.response const r = res.response
@ -362,7 +439,9 @@ export default class {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
throw new Error("lnd node is currently out of sync") 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 res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from)
const providerDst = this.liquidProvider.GetProviderDestination() const providerDst = this.liquidProvider.GetProviderDestination()
return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } 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<SendCoinsResponse> { 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") // console.log("Paying address")
if (this.outgoingOpsLocked) { if (this.outgoingOpsLocked) {
this.log("outgoing ops locked, rejecting payment request") this.log("outgoing ops locked, rejecting payment request")
@ -494,6 +577,9 @@ export default class {
} }
async GetBalance(): Promise<BalanceInfo> { // TODO: remove this async GetBalance(): Promise<BalanceInfo> { // TODO: remove this
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { confirmedBalance: 0, unconfirmedBalance: 0, totalBalance: 0, channelsBalance: [] }
}
// console.log("Getting balance") // console.log("Getting balance")
const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
@ -510,17 +596,26 @@ export default class {
} }
async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> { async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { forwardingEvents: [], lastOffsetIndex: indexOffset }
}
// console.log("Getting forwarding history") // console.log("Getting forwarding history")
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata()) const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata())
return response return response
} }
async GetAllPaidInvoices(max: number) { async GetAllPaidInvoices(max: number) {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { invoices: [] }
}
// console.log("Getting all paid 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()) const res = await this.lightning.listInvoices({ indexOffset: 0n, numMaxInvoices: BigInt(max), pendingOnly: false, reversed: true, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata())
return res.response return res.response
} }
async GetAllPayments(max: number) { async GetAllPayments(max: number) {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return { payments: [] }
}
// console.log("Getting all 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 }) const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset: 0n, maxPayments: BigInt(max), reversed: true, creationDateEnd: 0n, creationDateStart: 0n })
return res.response return res.response
@ -539,6 +634,9 @@ export default class {
} }
async GetLatestPaymentIndex(from = 0) { async GetLatestPaymentIndex(from = 0) {
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return from
}
// console.log("Getting latest payment index") // console.log("Getting latest payment index")
let indexOffset = BigInt(from) let indexOffset = BigInt(from)
while (true) { while (true) {

View file

@ -185,7 +185,7 @@ export default class {
return invoice 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 app = await this.storage.applicationStorage.GetApplication(appId)
const log = getLogger({ appName: app.name }) const log = getLogger({ appName: app.name })
const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier) 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, callbackUrl: cbUrl, expiry: expiry, expectedPayer: payer.user, linkedApplication: app, zapInfo,
offerId: req.offer_string, payerData: req.payer_data?.data, rejectUnauthorized: req.rejectUnauthorized, offerId: req.offer_string, payerData: req.payer_data?.data, rejectUnauthorized: req.rejectUnauthorized,
token: req.token, 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) const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts)
return { return {

View file

@ -10,11 +10,10 @@ import { AddressPaidCb, ChannelEventCb, HtlcCb, InvoicePaidCb, NewBlockCb } from
import { ERROR, getLogger, PubLogger } from "../helpers/logger.js" import { ERROR, getLogger, PubLogger } from "../helpers/logger.js"
import AppUserManager from "./appUserManager.js" import AppUserManager from "./appUserManager.js"
import { Application } from '../storage/entity/Application.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 { UnsignedEvent } from 'nostr-tools'
import { NostrEvent, NostrSend } from '../nostr/handler.js' import { NostrSend } from '../nostr/handler.js'
import MetricsManager from '../metrics/index.js' import MetricsManager from '../metrics/index.js'
import { LoggedEvent } from '../storage/eventsLog.js'
import { LiquidityProvider } from "./liquidityProvider.js" import { LiquidityProvider } from "./liquidityProvider.js"
import { LiquidityManager } from "./liquidityManager.js" import { LiquidityManager } from "./liquidityManager.js"
import { Utils } from "../helpers/utilsWrapper.js" import { Utils } from "../helpers/utilsWrapper.js"
@ -213,6 +212,11 @@ export default class {
addressPaidCb: AddressPaidCb = (txOutput, address, amount, used) => { addressPaidCb: AddressPaidCb = (txOutput, address, amount, used) => {
return this.storage.StartTransaction(async tx => { 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 { blockHeight } = await this.lnd.GetInfo()
const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx) const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx)
if (!userAddress) { if (!userAddress) {
@ -291,6 +295,14 @@ export default class {
} catch (err: any) { } catch (err: any) {
log(ERROR, "cannot create zap receipt", err.message || "") 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.liquidityManager.afterInInvoicePaid()
this.utils.stateBundler.AddTxPoint('invoiceWasPaid', amount, { used, from: 'system', timeDiscount: true }, userInvoice.linkedApplication.app_id) this.utils.stateBundler.AddTxPoint('invoiceWasPaid', amount, { used, from: 'system', timeDiscount: true }, userInvoice.linkedApplication.app_id)
} catch (err: any) { } 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) 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() { async ResetNostr() {
const apps = await this.storage.applicationStorage.GetApplications() const apps = await this.storage.applicationStorage.GetApplications()
const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0]

View file

@ -40,7 +40,7 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM
const mainHandler = new Main(settingsManager, storageManager, adminManager, utils, unlocker) const mainHandler = new Main(settingsManager, storageManager, adminManager, utils, unlocker)
adminManager.setLND(mainHandler.lnd) adminManager.setLND(mainHandler.lnd)
await mainHandler.lnd.Warmup() 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) const sanityChecker = new SanityChecker(storageManager, mainHandler.lnd)
await sanityChecker.VerifyEventsLog() await sanityChecker.VerifyEventsLog()
} }

View file

@ -71,6 +71,10 @@ export class LiquidityManager {
} }
afterInInvoicePaid = async () => { afterInInvoicePaid = async () => {
// Skip channel ordering if using only liquidity provider
if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
return
}
try { try {
await this.orderChannelIfNeeded() await this.orderChannelIfNeeded()
} catch (e: any) { } catch (e: any) {
@ -91,6 +95,10 @@ export class LiquidityManager {
afterOutInvoicePaid = async () => { } afterOutInvoicePaid = async () => { }
shouldDrainProvider = async () => { shouldDrainProvider = async () => {
// Skip draining when bypass is enabled
if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
return
}
const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable()
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
const drainable = Math.min(maxW, remote) const drainable = Math.min(maxW, remote)
@ -148,6 +156,10 @@ export class LiquidityManager {
shouldOpenChannel = async (): Promise<{ shouldOpen: false } | { shouldOpen: true, maxSpendable: number }> => { 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 const threshold = this.settings.getSettings().lspSettings.channelThreshold
if (threshold === 0) { if (threshold === 0) {
return { shouldOpen: false } return { shouldOpen: false }

View file

@ -164,7 +164,13 @@ export class OfferManager {
payerData: offerReq.payer_data 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) { if (!offerInvoice.success) {
const code = offerInvoice.code const code = offerInvoice.code
@ -198,7 +204,7 @@ export class OfferManager {
return 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 const { amount_sats: amount, offer } = offerReq
if (!amount || isNaN(amount) || amount < 10 || amount > remote) { if (!amount || isNaN(amount) || amount < 10 || amount > remote) {
return { success: false, code: 5, max: remote } return { success: false, code: 5, max: remote }
@ -207,17 +213,17 @@ export class OfferManager {
http_callback_url: "", payer_identifier: offer, receiver_identifier: offer, http_callback_url: "", payer_identifier: offer, receiver_identifier: offer,
invoice_req: { amountSats: amount, memo: memo || "Default CLINK Offer", zap: offerReq.zap, expiry }, invoice_req: { amountSats: amount, memo: memo || "Default CLINK Offer", zap: offerReq.zap, expiry },
offer_string: 'offer' offer_string: 'offer'
}) }, clinkRequester)
return { success: true, invoice: res.invoice } 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 { amount_sats: amount, offer } = offerReq
const userOffer = await this.storage.offerStorage.GetOffer(offer) const userOffer = await this.storage.offerStorage.GetOffer(offer)
const expiry = offerReq.expires_in_seconds ? offerReq.expires_in_seconds : undefined const expiry = offerReq.expires_in_seconds ? offerReq.expires_in_seconds : undefined
if (!userOffer) { 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.app_user_id === userOffer.offer_id) {
if (userOffer.price_sats !== 0 || userOffer.payer_data) { if (userOffer.price_sats !== 0 || userOffer.payer_data) {
@ -250,20 +256,28 @@ export class OfferManager {
offer_string: offer, offer_string: offer,
rejectUnauthorized: userOffer.rejectUnauthorized, rejectUnauthorized: userOffer.rejectUnauthorized,
token: userOffer.bearer_token token: userOffer.bearer_token
}) }, clinkRequester)
return { success: true, invoice: res.invoice } 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 { try {
const { remote } = await this.lnd.ChannelBalance() // When bypass is enabled, use provider balance instead of LND channel balance
let maxSendable = remote let maxSendable = 0
if (remote === 0 && (await this.liquidityManager.liquidityProvider.IsReady())) { if (this.liquidityManager.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) {
maxSendable = 10_000_000 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(':') const split = offerReq.offer.split(':')
if (split.length === 1) { if (split.length === 1) {
return this.HandleUserOffer(offerReq, appId, maxSendable) return this.HandleUserOffer(offerReq, appId, maxSendable, clinkRequester)
} else if (split[0] === 'p') { } else if (split[0] === 'p') {
const product = await this.productManager.NewProductInvoice(split[1]) const product = await this.productManager.NewProductInvoice(split[1])
return { success: true, invoice: product.invoice } return { success: true, invoice: product.invoice }

View file

@ -115,6 +115,11 @@ export default class {
} }
checkPendingLndPayment = async (log: PubLogger, p: UserInvoicePayment) => { 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 decoded = await this.lnd.DecodeInvoice(p.invoice)
const payment = await this.lnd.GetPaymentFromHash(decoded.paymentHash) const payment = await this.lnd.GetPaymentFromHash(decoded.paymentHash)
if (!payment || payment.paymentHash !== decoded.paymentHash) { if (!payment || payment.paymentHash !== decoded.paymentHash) {

View file

@ -172,7 +172,8 @@ export const LoadLiquiditySettingsFromEnv = (dbEnv: Record<string, string | unde
//const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB === "null" ? "" : (process.env.LIQUIDITY_PROVIDER_PUB || "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e") //const liquidityProviderPub = process.env.LIQUIDITY_PROVIDER_PUB === "null" ? "" : (process.env.LIQUIDITY_PROVIDER_PUB || "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e")
const liquidityProviderPub = chooseEnv("LIQUIDITY_PROVIDER_PUB", dbEnv, "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e", addToDb) const liquidityProviderPub = chooseEnv("LIQUIDITY_PROVIDER_PUB", dbEnv, "76ed45f00cea7bac59d8d0b7d204848f5319d7b96c140ffb6fcbaaab0a13d44e", addToDb)
const disableLiquidityProvider = chooseEnvBool("DISABLE_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) || liquidityProviderPub === "null" const disableLiquidityProvider = chooseEnvBool("DISABLE_LIQUIDITY_PROVIDER", dbEnv, false, addToDb) || liquidityProviderPub === "null"
return { liquidityProviderPub, useOnlyLiquidityProvider: false, disableLiquidityProvider } const useOnlyLiquidityProvider = chooseEnvBool("USE_ONLY_LIQUIDITY_PROVIDER", dbEnv, false, addToDb)
return { liquidityProviderPub, useOnlyLiquidityProvider, disableLiquidityProvider }
} }

View file

@ -55,6 +55,11 @@ export class Unlocker {
} }
Unlock = async (): Promise<'created' | 'unlocked' | 'noaction'> => { 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() const { lndCert, macaroon } = this.getCreds()
if (macaroon === "") { if (macaroon === "") {
const { ln, pub } = await this.InitFlow(lndCert) const { ln, pub } = await this.InitFlow(lndCert)

View file

@ -54,6 +54,12 @@ export class Watchdog {
} }
} }
StartWatching = async () => { 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.log("Starting watchdog")
this.startedAtUnix = Math.floor(Date.now() / 1000) this.startedAtUnix = Math.floor(Date.now() / 1000)
const info = await this.lnd.GetInfo() const info = await this.lnd.GetInfo()
@ -205,6 +211,10 @@ export class Watchdog {
} }
PaymentRequested = async () => { PaymentRequested = async () => {
// Skip watchdog check when bypass is enabled
if (this.liquidProvider.getSettings().useOnlyLiquidityProvider) {
return
}
if (!this.ready) { if (!this.ready) {
throw new Error("Watchdog not ready") throw new Error("Watchdog not ready")
} }

View file

@ -80,6 +80,12 @@ export class UserReceivingInvoice {
}) })
liquidityProvider?: string liquidityProvider?: string
@Column({ nullable: true })
clink_requester_pub?: string
@Column({ nullable: true })
clink_requester_event_id?: string
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

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

@ -27,13 +27,14 @@ import { UserAccess1759426050669 } from './1759426050669-user_access.js'
import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js' import { AddBlindToUserOffer1760000000000 } from './1760000000000-add_blind_to_user_offer.js'
import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js' import { ApplicationAvatarUrl1761000001000 } from './1761000001000-application_avatar_url.js'
import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js' import { AdminSettings1761683639419 } from './1761683639419-admin_settings.js'
import { ClinkRequester1765497600000 } from './1765497600000-clink_requester.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513,
InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175, 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 allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => { /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {

View file

@ -14,7 +14,7 @@ import { Application } from './entity/Application.js';
import TransactionsQueue from "./db/transactionsQueue.js"; import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js'; import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.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<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 const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
dbs: StorageInterface dbs: StorageInterface
@ -129,7 +129,9 @@ export default class {
offer_id: options.offerId, offer_id: options.offerId,
payer_data: options.payerData, payer_data: options.payerData,
rejectUnauthorized: options.rejectUnauthorized, rejectUnauthorized: options.rejectUnauthorized,
bearer_token: options.token bearer_token: options.token,
clink_requester_pub: options.clinkRequesterPub,
clink_requester_event_id: options.clinkRequesterEventId
}, txId) }, txId)
} }