From 1623777c1f3ffe7608dc1795107b1c09c5b7ad68 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 31 Oct 2025 17:34:40 +0000 Subject: [PATCH 01/29] fix lnd crash --- src/services/lnd/lnd.ts | 57 +++++++++++++++++++++++++++++++---- src/services/main/watchdog.ts | 7 ++++- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 254ccb7b..b68975b5 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -95,9 +95,10 @@ export default class { } async Warmup() { + // console.log("Warming up LND") this.SubscribeAddressPaid() this.SubscribeInvoicePaid() - this.SubscribeNewBlock() + await this.SubscribeNewBlock() this.SubscribeHtlcEvents() this.SubscribeChannelEvents() const now = Date.now() @@ -119,20 +120,24 @@ export default class { } async GetInfo(): Promise { + // console.log("Getting info") const res = await this.lightning.getInfo({}, DeadLineMetadata()) return res.response } async ListPendingChannels(): Promise { + // console.log("Listing pending channels") const res = await this.lightning.pendingChannels({ includeRawTx: false }, DeadLineMetadata()) return res.response } async ListChannels(peerLookup = false): Promise { + // console.log("Listing channels") const res = await this.lightning.listChannels({ activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0), peerAliasLookup: peerLookup }, DeadLineMetadata()) return res.response } async ListClosedChannels(): Promise { + // console.log("Listing closed channels") const res = await this.lightning.closedChannels({ abandoned: true, breach: true, @@ -145,6 +150,7 @@ export default class { } async Health(): Promise { + // console.log("Checking health") if (!this.ready) { throw new Error("not ready") } @@ -155,6 +161,7 @@ export default class { } RestartStreams() { + // console.log("Restarting streams") if (!this.ready) { return } @@ -164,7 +171,7 @@ export default class { await this.Health() this.log("LND is back online") clearInterval(interval) - this.Warmup() + await this.Warmup() } catch (err) { this.log("LND still dead, will try again in", deadLndRetrySeconds, "seconds") } @@ -172,6 +179,7 @@ export default class { } async SubscribeChannelEvents() { + // console.log("Subscribing to channel events") const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal }) stream.responses.onMessage(async channel => { const channels = await this.ListChannels() @@ -186,6 +194,7 @@ export default class { } async SubscribeHtlcEvents() { + // console.log("Subscribing to htlc events") const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal }) stream.responses.onMessage(htlc => { this.htlcCb(htlc) @@ -199,20 +208,22 @@ export default class { } async SubscribeNewBlock() { + // console.log("Subscribing to new block") const { blockHeight } = await this.GetInfo() const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal }) stream.responses.onMessage(block => { this.newBlockCb(block.height) }) stream.responses.onError(error => { - this.log("Error with onchain tx stream") + this.log("Error with new block stream") }) stream.responses.onComplete(() => { - this.log("onchain tx stream closed") + this.log("new block stream closed") }) } SubscribeAddressPaid(): void { + // console.log("Subscribing to address paid") const stream = this.lightning.subscribeTransactions({ account: "", endHeight: 0, @@ -239,6 +250,7 @@ export default class { } SubscribeInvoicePaid(): void { + // console.log("Subscribing to invoice paid") const stream = this.lightning.subscribeInvoices({ settleIndex: BigInt(this.latestKnownSettleIndex), addIndex: 0n, @@ -249,17 +261,25 @@ export default class { this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat), 'lnd') } }) + let restarted = false stream.responses.onError(error => { this.log("Error with invoice stream") + if (!restarted) { + restarted = true + this.RestartStreams() + } }) stream.responses.onComplete(() => { this.log("invoice stream closed") - this.RestartStreams() + if (!restarted) { + restarted = true + this.RestartStreams() + } }) } async NewAddress(addressType: Types.AddressType, { useProvider, from }: TxActionOptions): Promise { - + // console.log("Creating new address") let lndAddressType: AddressType switch (addressType) { case Types.AddressType.NESTED_PUBKEY_HASH: @@ -289,6 +309,7 @@ export default class { } async NewInvoice(value: number, memo: string, expiry: number, { useProvider, from }: TxActionOptions, blind = false): Promise { + // console.log("Creating new invoice") if (useProvider) { console.log("using provider") const invoice = await this.liquidProvider.AddInvoice(value, memo, from, expiry) @@ -306,6 +327,7 @@ export default class { } async DecodeInvoice(paymentRequest: string): Promise { + // console.log("Decoding invoice") const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata()) return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } } @@ -319,11 +341,13 @@ export default class { } async ChannelBalance(): Promise<{ local: number, remote: number }> { + // console.log("Getting channel balance") const res = await this.lightning.channelBalance({}) const r = res.response return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 } } async PayInvoice(invoice: string, amount: number, feeLimit: number, decodedAmount: number, { useProvider, from }: TxActionOptions, paymentIndexCb?: (index: number) => void): Promise { + // console.log("Paying invoice") if (this.outgoingOpsLocked) { this.log("outgoing ops locked, rejecting payment request") throw new Error("lnd node is currently out of sync") @@ -370,6 +394,7 @@ export default class { } async EstimateChainFees(address: string, amount: number, targetConf: number): Promise { + // console.log("Estimating chain fees") await this.Health() const res = await this.lightning.estimateFee({ addrToAmount: { [address]: BigInt(amount) }, @@ -382,6 +407,7 @@ export default class { } async PayAddress(address: string, amount: number, satPerVByte: number, label = "", { useProvider, from }: TxActionOptions): Promise { + // console.log("Paying address") if (this.outgoingOpsLocked) { this.log("outgoing ops locked, rejecting payment request") throw new Error("lnd node is currently out of sync") @@ -401,16 +427,19 @@ export default class { } async GetTransactions(startHeight: number): Promise { + // console.log("Getting transactions") const res = await this.lightning.getTransactions({ startHeight, endHeight: 0, account: "" }, DeadLineMetadata()) return res.response } async GetChannelInfo(chanId: string) { + // console.log("Getting channel info") const res = await this.lightning.getChanInfo({ chanId, chanPoint: "" }, DeadLineMetadata()) return res.response } async UpdateChannelPolicy(chanPoint: string, policy: Types.ChannelPolicy) { + // console.log("Updating channel policy") const split = chanPoint.split(':') const res = await this.lightning.updateChannelPolicy({ @@ -428,16 +457,19 @@ export default class { } async GetChannelBalance() { + // console.log("Getting channel balance") const res = await this.lightning.channelBalance({}, DeadLineMetadata()) return res.response } async GetWalletBalance() { + // console.log("Getting wallet balance") const res = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) return res.response } async GetTotalBalace() { + // console.log("Getting total balance") const walletBalance = await this.GetWalletBalance() const confirmedWalletBalance = Number(walletBalance.confirmedBalance) this.utils.stateBundler.AddBalancePoint('walletBalance', confirmedWalletBalance) @@ -452,6 +484,7 @@ export default class { } async GetBalance(): Promise { // TODO: remove this + // console.log("Getting balance") const wRes = await this.lightning.walletBalance({ account: "", minConfs: 1 }, DeadLineMetadata()) const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response const { response } = await this.lightning.listChannels({ @@ -467,20 +500,24 @@ export default class { } async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise { + // 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) { + // 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) { + // 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 } async GetPayment(paymentIndex: number) { + // console.log("Getting payment") if (paymentIndex === 0) { throw new Error("payment index starts from 1") } @@ -492,6 +529,7 @@ export default class { } async GetLatestPaymentIndex(from = 0) { + // console.log("Getting latest payment index") let indexOffset = BigInt(from) while (true) { const res = await this.lightning.listPayments({ countTotalPayments: false, includeIncomplete: false, indexOffset, maxPayments: 0n, reversed: false, creationDateEnd: 0n, creationDateStart: 0n }, DeadLineMetadata()) @@ -503,6 +541,7 @@ export default class { } async ConnectPeer(addr: { pubkey: string, host: string }) { + // console.log("Connecting to peer") const res = await this.lightning.connectPeer({ addr, perm: true, @@ -512,6 +551,7 @@ export default class { } async GetPaymentFromHash(paymentHash: string): Promise { + // console.log("Getting payment from hash") const abortController = new AbortController() const stream = this.router.trackPaymentV2({ paymentHash: Buffer.from(paymentHash, 'hex'), @@ -533,11 +573,13 @@ export default class { } async GetTx(txid: string) { + // console.log("Getting transaction") const res = await this.walletKit.getTransaction({ txid }, DeadLineMetadata()) return res.response } async AddPeer(pub: string, host: string, port: number) { + // console.log("Adding peer") const res = await this.lightning.connectPeer({ addr: { pubkey: pub, @@ -550,11 +592,13 @@ export default class { } async ListPeers() { + // console.log("Listing peers") const res = await this.lightning.listPeers({ latestError: true }, DeadLineMetadata()) return res.response } async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number, satsPerVByte: number): Promise { + // console.log("Opening channel") const abortController = new AbortController() const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats, satsPerVByte) const stream = this.lightning.openChannel(req, { abort: abortController.signal }) @@ -575,6 +619,7 @@ export default class { } async CloseChannel(fundingTx: string, outputIndex: number, force: boolean, satPerVByte: number): Promise { + // console.log("Closing channel") const stream = this.lightning.closeChannel({ deliveryAddress: "", force: force, diff --git a/src/services/main/watchdog.ts b/src/services/main/watchdog.ts index 07e59662..50215045 100644 --- a/src/services/main/watchdog.ts +++ b/src/services/main/watchdog.ts @@ -179,7 +179,12 @@ export class Watchdog { StartCheck = async () => { this.latestCheckStart = Date.now() - await this.updateAccumulatedHtlcFees() + try { + await this.updateAccumulatedHtlcFees() + } catch (err: any) { + this.log("Error updating accumulated htlc fees", err.message || err) + return + } const totalUsersBalance = await this.storage.paymentStorage.GetTotalUsersBalance() this.utils.stateBundler.AddBalancePoint('usersBalance', totalUsersBalance) const { totalExternal, otherExternal } = await this.getAggregatedExternalBalance() From 89154933b9cc62df42dcf46a44671ca007b1d081 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 31 Oct 2025 18:25:40 +0000 Subject: [PATCH 02/29] unlock on reconnect --- src/services/lnd/lnd.ts | 8 +++++--- src/services/main/index.ts | 2 +- src/services/main/unlocker.ts | 4 ++-- src/tests/networkSetup.ts | 4 ++-- src/tests/testBase.ts | 4 ++-- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index b68975b5..6aaa7f9b 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -21,7 +21,7 @@ import { Utils } from '../helpers/utilsWrapper.js'; import { TxPointSettings } from '../storage/tlv/stateBundler.js'; import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js'; const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline }) -const deadLndRetrySeconds = 5 +const deadLndRetrySeconds = 20 type TxActionOptions = { useProvider: boolean, from: 'user' | 'system' } export default class { lightning: LightningClient @@ -43,9 +43,11 @@ export default class { outgoingOpsLocked = false liquidProvider: LiquidityProvider utils: Utils - constructor(settings: LndSettings, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { + unlockLnd: () => Promise + constructor(settings: LndSettings, liquidProvider: LiquidityProvider, unlockLnd: () => Promise, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) { this.settings = settings this.utils = utils + this.unlockLnd = unlockLnd this.addressPaidCb = addressPaidCb this.invoicePaidCb = invoicePaidCb this.newBlockCb = newBlockCb @@ -168,7 +170,7 @@ export default class { this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds") const interval = setInterval(async () => { try { - await this.Health() + await this.unlockLnd() this.log("LND is back online") clearInterval(interval) await this.Warmup() diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 71c64592..5c5fef16 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -74,7 +74,7 @@ export default class { const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.liquiditySettings.liquidityProviderPub, b) this.liquidityProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.utils, this.invoicePaidCb, updateProviderBalance) this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider) - this.lnd = new LND(settings.lndSettings, this.liquidityProvider, this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb) + this.lnd = new LND(settings.lndSettings, this.liquidityProvider, () => this.unlocker.Unlock(), this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb) this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker) this.metricsManager = new MetricsManager(this.storage, this.lnd) diff --git a/src/services/main/unlocker.ts b/src/services/main/unlocker.ts index b104ffd3..7221d183 100644 --- a/src/services/main/unlocker.ts +++ b/src/services/main/unlocker.ts @@ -301,12 +301,12 @@ export class Unlocker { GetWalletPassword = () => { const path = this.settings.walletPasswordPath - let password = Buffer.alloc(0) + let password: Buffer | null = null try { password = fs.readFileSync(path) } catch { } - if (password.length === 0) { + if (!password || password.length === 0) { this.log("no wallet password configured, using wallet secret") const secret = this.GetWalletSecret(false) if (secret === "") { diff --git a/src/tests/networkSetup.ts b/src/tests/networkSetup.ts index 5944a1fc..40f0e58b 100644 --- a/src/tests/networkSetup.ts +++ b/src/tests/networkSetup.ts @@ -14,8 +14,8 @@ export const setupNetwork = async (): Promise => { await core.InitAddress() await core.Mine(1) const setupUtils = new Utils({ dataDir: settings.storageSettings.dataDir, allowResetMetricsStorages: settings.allowResetMetricsStorages }) - const alice = new LND(settings.lndSettings, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) - const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const alice = new LND(settings.lndSettings, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await tryUntil(async i => { const peers = await alice.ListPeers() if (peers.peers.length > 0) { diff --git a/src/tests/testBase.ts b/src/tests/testBase.ts index 95454a95..fdd599b9 100644 --- a/src/tests/testBase.ts +++ b/src/tests/testBase.ts @@ -78,11 +78,11 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await externalAccessToOtherLnd.Warmup() const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode } - const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) + const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) await externalAccessToThirdLnd.Warmup() From 203dde3d389f5810e00f08b5fbc9b06cb6ec7a9e Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 31 Oct 2025 18:33:28 +0000 Subject: [PATCH 03/29] dont reconnect after abort --- src/services/lnd/lnd.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 6aaa7f9b..0f9daaa9 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -164,7 +164,7 @@ export default class { RestartStreams() { // console.log("Restarting streams") - if (!this.ready) { + if (!this.ready || this.abortController.signal.aborted) { return } this.log("LND is dead, will try to reconnect in", deadLndRetrySeconds, "seconds") From 3e45e4ec90e5b77a73166aac372b0d241d6bc456 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 31 Oct 2025 19:37:21 +0000 Subject: [PATCH 04/29] disable new invoice check --- src/services/storage/paymentStorage.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 5f3d15a9..ef29957c 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -90,7 +90,7 @@ export default class { take }, txId); items.push(...firstBatch); - } + } const needMore = take - items.length // If need more, fetch higher paid_at_unix @@ -134,7 +134,7 @@ export default class { } async RemoveUserInvoices(userId: string, txId?: string) { - return this.dbs.Delete('UserReceivingInvoice', { user: { user_id: userId } }, txId) + return this.dbs.Delete('UserReceivingInvoice', { user: { user_id: userId } }, txId) } async GetAddressOwner(address: string, txId?: string): Promise { @@ -401,7 +401,7 @@ export default class { ]) const receivingTransactions = await Promise.all(receivingAddresses.map(addr => this.dbs.Find('AddressReceivingTransaction', { where: { user_address: { serial_id: addr.serial_id }, ...time } }))) - return { + return { receivingInvoices, receivingAddresses, receivingTransactions, outgoingInvoices, outgoingTransactions, userToUser @@ -419,8 +419,8 @@ export default class { async VerifyDbEvent(e: LoggedEvent) { switch (e.type) { - case "new_invoice": - return orFail(this.dbs.FindOne('UserReceivingInvoice', { where: { invoice: e.data, user: { user_id: e.userId } } })) + /* case "new_invoice": + return orFail(this.dbs.FindOne('UserReceivingInvoice', { where: { invoice: e.data, user: { user_id: e.userId } } })) */ case 'new_address': return orFail(this.dbs.FindOne('UserReceivingAddress', { where: { address: e.data, user: { user_id: e.userId } } })) case 'invoice_paid': From eccb754790f9265038c887157844d6793debe793 Mon Sep 17 00:00:00 2001 From: "Justin (shocknet)" <34176400+shocknet-justin@users.noreply.github.com> Date: Sun, 16 Nov 2025 12:31:44 -0500 Subject: [PATCH 05/29] Update DOCKER.md --- DOCKER.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 8ffbb87d..1e4bebae 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -1,8 +1,5 @@ # Docker Installation -> [!WARNING] -> The Docker deployment method is currently unmaintained and may not work as expected. Help is wanted! If you are a Docker enjoyer, please consider contributing to this deployment method. - 1. Pull the Docker image: ```ssh @@ -21,4 +18,5 @@ docker run -d \ -v $HOME/.lnd:/root/.lnd \ ghcr.io/shocknet/lightning-pub:latest ``` -Network host is used so the service can reach a local LND via localhost. LND is assumed to be under the users home folder, update this location as needed. + +Network host is used so the service can reach a local LND via localhost. LND is assumed to be under the users home folder, update these resources as needed. From d319c1d4be754dd93848dd9d2098de94d0980fa7 Mon Sep 17 00:00:00 2001 From: shocknet-justin Date: Mon, 17 Nov 2025 00:04:02 -0500 Subject: [PATCH 06/29] start9 action --- .github/workflows/push.yml | 39 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index e70e0e3d..d9440569 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -24,6 +24,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + - name: Log in to the Container registry uses: docker/login-action@v2 with: @@ -50,6 +56,7 @@ jobs: context: . file: ./Dockerfile push: true + platforms: linux/amd64,linux/arm64 tags: | ghcr.io/${{ github.repository_owner }}/lightning-pub:latest ghcr.io/${{ github.repository_owner }}/lightning-pub:master @@ -58,8 +65,9 @@ jobs: - name: Capture image digest id: capture-digest run: | - DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/${{ github.repository_owner }}/lightning-pub:latest | cut -d'@' -f2) - echo "Raw Digest is $DIGEST" + # For multi-arch builds, use the digest from build output + DIGEST="${{ steps.build-and-push.outputs.digest }}" + echo "Multi-arch manifest digest: $DIGEST" echo "digest=$DIGEST" >> $GITHUB_OUTPUT - name: Debug Print Digest @@ -71,3 +79,30 @@ jobs: subject-digest: ${{ steps.capture-digest.outputs.digest }} subject-name: ghcr.io/${{ github.repository_owner }}/lightning-pub push-to-registry: true + + - name: Trigger StartOS package build + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' + env: + STARTOS_BUILD_TRIGGER: ${{ secrets.STARTOS_BUILD_TRIGGER }} + run: | + if [ -z "$STARTOS_BUILD_TRIGGER" ]; then + echo "⚠️ STARTOS_BUILD_TRIGGER not configured, skipping StartOS build trigger" + exit 0 + fi + + echo "🚀 Triggering StartOS package build with digest: ${{ steps.capture-digest.outputs.digest }}" + + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $STARTOS_BUILD_TRIGGER" \ + https://api.github.com/repos/shocknet/start9-LightningPub/dispatches \ + -d "{ + \"event_type\": \"docker-image-updated\", + \"client_payload\": { + \"docker_tag\": \"latest\", + \"digest\": \"${{ steps.capture-digest.outputs.digest }}\", + \"commit\": \"${{ github.sha }}\" + } + }" + + echo "✓ StartOS build triggered" \ No newline at end of file From c8ede119d6cc17074ca2642fb779123d19bb7a68 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 19 Nov 2025 15:46:35 +0000 Subject: [PATCH 07/29] payment stream + unreachable provider + fess calc fix --- proto/autogenerated/client.md | 17 ++++ proto/autogenerated/go/http_client.go | 2 + proto/autogenerated/go/types.go | 23 +++++- proto/autogenerated/ts/http_client.ts | 1 + proto/autogenerated/ts/nostr_client.ts | 16 ++++ proto/autogenerated/ts/nostr_transport.ts | 16 ++++ proto/autogenerated/ts/types.ts | 78 +++++++++++++++++- proto/service/methods.proto | 7 ++ proto/service/structs.proto | 9 +++ src/services/lnd/lnd.ts | 8 +- src/services/main/appUserManager.ts | 23 ++++-- src/services/main/applicationManager.ts | 28 ++++--- src/services/main/liquidityManager.ts | 25 ++++-- src/services/main/liquidityProvider.ts | 98 ++++++++++++++++++----- src/services/main/paymentManager.ts | 71 +++++++++++++--- src/services/serverMethods/index.ts | 7 ++ 16 files changed, 365 insertions(+), 64 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index a4116e6c..5bad6c03 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -275,6 +275,11 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayInvoiceRequest](#PayInvoiceRequest) - output: [PayInvoiceResponse](#PayInvoiceResponse) +- PayInvoiceStream + - auth type: __User__ + - input: [PayInvoiceRequest](#PayInvoiceRequest) + - output: [InvoicePaymentStream](#InvoicePaymentStream) + - PingSubProcesses - auth type: __Metrics__ - This methods has an __empty__ __request__ body @@ -860,6 +865,13 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayInvoiceRequest](#PayInvoiceRequest) - output: [PayInvoiceResponse](#PayInvoiceResponse) +- PayInvoiceStream + - auth type: __User__ + - http method: __post__ + - http route: __/api/user/invoice/pay/stream__ + - input: [PayInvoiceRequest](#PayInvoiceRequest) + - output: [InvoicePaymentStream](#InvoicePaymentStream) + - PingSubProcesses - auth type: __Metrics__ - http method: __post__ @@ -1256,6 +1268,9 @@ The nostr server will send back a message response, and inside the body there wi - __token__: _string_ - __url__: _string_ +### InvoicePaymentStream + - __update__: _[InvoicePaymentStream_update](#InvoicePaymentStream_update)_ + ### LatestBundleMetricReq - __limit__: _number_ *this field is optional @@ -1460,12 +1475,14 @@ The nostr server will send back a message response, and inside the body there wi ### PayAppUserInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional + - __fee_limit_sats__: _number_ *this field is optional - __invoice__: _string_ - __user_identifier__: _string_ ### PayInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional + - __fee_limit_sats__: _number_ *this field is optional - __invoice__: _string_ ### PayInvoiceResponse diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index 8c20a0a6..7072488f 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -121,6 +121,7 @@ type Client struct { PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) + PayInvoiceStream func(req PayInvoiceRequest) (*InvoicePaymentStream, error) PingSubProcesses func() error RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) ResetDebit func(req DebitOperation) error @@ -1835,6 +1836,7 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, + // server streaming method: PayInvoiceStream not implemented PingSubProcesses: func() error { auth, err := params.RetrieveMetricsAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index e8e90df5..7a744c79 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -341,6 +341,9 @@ type HttpCreds struct { Token string `json:"token"` Url string `json:"url"` } +type InvoicePaymentStream struct { + Update *InvoicePaymentStream_update `json:"update"` +} type LatestBundleMetricReq struct { Limit int64 `json:"limit"` } @@ -545,13 +548,15 @@ type PayAddressResponse struct { type PayAppUserInvoiceRequest struct { Amount int64 `json:"amount"` Debit_npub string `json:"debit_npub"` + Fee_limit_sats int64 `json:"fee_limit_sats"` Invoice string `json:"invoice"` User_identifier string `json:"user_identifier"` } type PayInvoiceRequest struct { - Amount int64 `json:"amount"` - Debit_npub string `json:"debit_npub"` - Invoice string `json:"invoice"` + Amount int64 `json:"amount"` + Debit_npub string `json:"debit_npub"` + Fee_limit_sats int64 `json:"fee_limit_sats"` + Invoice string `json:"invoice"` } type PayInvoiceResponse struct { Amount_paid int64 `json:"amount_paid"` @@ -751,6 +756,18 @@ type DebitRule_rule struct { Expiration_rule *DebitExpirationRule `json:"expiration_rule"` Frequency_rule *FrequencyRule `json:"frequency_rule"` } +type InvoicePaymentStream_update_type string + +const ( + ACK InvoicePaymentStream_update_type = "ack" + DONE InvoicePaymentStream_update_type = "done" +) + +type InvoicePaymentStream_update struct { + Type InvoicePaymentStream_update_type `json:"type"` + Ack *Empty `json:"ack"` + Done *PayInvoiceResponse `json:"done"` +} type LiveDebitRequest_debit_type string const ( diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 9c37e1b3..66f27b92 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -881,6 +881,7 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, + PayInvoiceStream: async (request: Types.PayInvoiceRequest, cb: (v:ResultError | ({ status: 'OK' }& Types.InvoicePaymentStream)) => void): Promise => { throw new Error('http streams are not supported')}, PingSubProcesses: async (): Promise => { const auth = await params.retrieveMetricsAuth() if (auth === null) throw new Error('retrieveMetricsAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 38795d01..002561bb 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -755,6 +755,22 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, + PayInvoiceStream: async (request: Types.PayInvoiceRequest, cb: (res:ResultError | ({ status: 'OK' }& Types.InvoicePaymentStream)) => void): Promise => { + const auth = await params.retrieveNostrUserAuth() + if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') + const nostrRequest: NostrRequest = {} + nostrRequest.body = request + subscribe(params.pubDestination, {rpcName:'PayInvoiceStream',authIdentifier:auth, ...nostrRequest }, (data) => { + if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) + if (data.status === 'OK') { + const result = data + if(!params.checkResult) return cb({ status: 'OK', ...result }) + const error = Types.InvoicePaymentStreamValidate(result) + if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) + } + return cb({ status: 'ERROR', reason: 'invalid response' }) + }) + }, PingSubProcesses: async (): Promise => { const auth = await params.retrieveNostrMetricsAuth() if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index cd9e708f..c0d08bd0 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -1190,6 +1190,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break + case 'PayInvoiceStream': + try { + if (!methods.PayInvoiceStream) throw new Error('method: PayInvoiceStream is not implemented') + const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) + stats.guard = process.hrtime.bigint() + authCtx = authContext + const request = req.body + const error = Types.PayInvoiceRequestValidate(request) + stats.validate = process.hrtime.bigint() + if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) + methods.PayInvoiceStream({rpcName:'PayInvoiceStream', ctx:authContext , req: request ,cb: (response, err) => { + stats.handle = process.hrtime.bigint() + if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)} else { res({status: 'OK', ...response});opts.metricsCallback([{ ...info, ...stats, ...authContext }])} + }}) + }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } + break case 'PingSubProcesses': try { if (!methods.PingSubProcesses) throw new Error('method: PingSubProcesses is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 239d0686..921f9a82 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -262,6 +262,9 @@ export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvo export type PayInvoice_Input = {rpcName:'PayInvoice', req: PayInvoiceRequest} export type PayInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) +export type PayInvoiceStream_Input = {rpcName:'PayInvoiceStream', req: PayInvoiceRequest, cb:(res: InvoicePaymentStream, err:Error|null)=> void} +export type PayInvoiceStream_Output = ResultError | { status: 'OK' } + export type PingSubProcesses_Input = {rpcName:'PingSubProcesses'} export type PingSubProcesses_Output = ResultError | { status: 'OK' } @@ -389,6 +392,7 @@ export type ServerMethods = { PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise + PayInvoiceStream?: (req: PayInvoiceStream_Input & {ctx: UserContext }) => Promise PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise ResetDebit?: (req: ResetDebit_Input & {ctx: UserContext }) => Promise @@ -1980,6 +1984,25 @@ export const HttpCredsValidate = (o?: HttpCreds, opts: HttpCredsOptions = {}, pa return null } +export type InvoicePaymentStream = { + update: InvoicePaymentStream_update +} +export const InvoicePaymentStreamOptionalFields: [] = [] +export type InvoicePaymentStreamOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + update_Options?: InvoicePaymentStream_updateOptions +} +export const InvoicePaymentStreamValidate = (o?: InvoicePaymentStream, opts: InvoicePaymentStreamOptions = {}, path: string = 'InvoicePaymentStream::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + const updateErr = InvoicePaymentStream_updateValidate(o.update, opts.update_Options, `${path}.update`) + if (updateErr !== null) return updateErr + + + return null +} + export type LatestBundleMetricReq = { limit?: number } @@ -3192,15 +3215,17 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr export type PayAppUserInvoiceRequest = { amount: number debit_npub?: string + fee_limit_sats?: number invoice: string user_identifier: string } -export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' -export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub'] +export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' | 'fee_limit_sats' +export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub', 'fee_limit_sats'] export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean + fee_limit_sats_CustomCheck?: (v?: number) => boolean invoice_CustomCheck?: (v: string) => boolean user_identifier_CustomCheck?: (v: string) => boolean } @@ -3214,6 +3239,9 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) + if ((o.fee_limit_sats || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('fee_limit_sats')) && typeof o.fee_limit_sats !== 'number') return new Error(`${path}.fee_limit_sats: is not a number`) + if (opts.fee_limit_sats_CustomCheck && !opts.fee_limit_sats_CustomCheck(o.fee_limit_sats)) return new Error(`${path}.fee_limit_sats: custom check failed`) + if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) @@ -3226,14 +3254,16 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o export type PayInvoiceRequest = { amount: number debit_npub?: string + fee_limit_sats?: number invoice: string } -export type PayInvoiceRequestOptionalField = 'debit_npub' -export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub'] +export type PayInvoiceRequestOptionalField = 'debit_npub' | 'fee_limit_sats' +export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub', 'fee_limit_sats'] export type PayInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean + fee_limit_sats_CustomCheck?: (v?: number) => boolean invoice_CustomCheck?: (v: string) => boolean } export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoiceRequestOptions = {}, path: string = 'PayInvoiceRequest::root.'): Error | null => { @@ -3246,6 +3276,9 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) + if ((o.fee_limit_sats || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('fee_limit_sats')) && typeof o.fee_limit_sats !== 'number') return new Error(`${path}.fee_limit_sats: is not a number`) + if (opts.fee_limit_sats_CustomCheck && !opts.fee_limit_sats_CustomCheck(o.fee_limit_sats)) return new Error(`${path}.fee_limit_sats: custom check failed`) + if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) @@ -4322,6 +4355,43 @@ export const DebitRule_ruleValidate = (o?: DebitRule_rule, opts:DebitRule_ruleOp if (frequency_ruleErr !== null) return frequency_ruleErr + break + default: + return new Error(path + ': unknown type '+ stringType) + } + return null +} +export enum InvoicePaymentStream_update_type { + ACK = 'ack', + DONE = 'done', +} +export const enumCheckInvoicePaymentStream_update_type = (e?: InvoicePaymentStream_update_type): boolean => { + for (const v in InvoicePaymentStream_update_type) if (e === v) return true + return false +} +export type InvoicePaymentStream_update = + {type:InvoicePaymentStream_update_type.ACK, ack:Empty}| + {type:InvoicePaymentStream_update_type.DONE, done:PayInvoiceResponse} + +export type InvoicePaymentStream_updateOptions = { + ack_Options?: EmptyOptions + done_Options?: PayInvoiceResponseOptions +} +export const InvoicePaymentStream_updateValidate = (o?: InvoicePaymentStream_update, opts:InvoicePaymentStream_updateOptions = {}, path: string = 'InvoicePaymentStream_update::root.'): Error | null => { + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + const stringType: string = o.type + switch (o.type) { + case InvoicePaymentStream_update_type.ACK: + const ackErr = EmptyValidate(o.ack, opts.ack_Options, `${path}.ack`) + if (ackErr !== null) return ackErr + + + break + case InvoicePaymentStream_update_type.DONE: + const doneErr = PayInvoiceResponseValidate(o.done, opts.done_Options, `${path}.done`) + if (doneErr !== null) return doneErr + + break default: return new Error(path + ': unknown type '+ stringType) diff --git a/proto/service/methods.proto b/proto/service/methods.proto index 4cff7379..eb253572 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -517,6 +517,13 @@ service LightningPub { option (nostr) = true; } + rpc PayInvoiceStream(structs.PayInvoiceRequest) returns (stream structs.InvoicePaymentStream){ + option (auth_type) = "User"; + option (http_method) = "post"; + option (http_route) = "/api/user/invoice/pay/stream"; + option (nostr) = true; + } + rpc GetPaymentState(structs.GetPaymentStateRequest) returns (structs.PaymentState){ option (auth_type) = "User"; option (http_method) = "post"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 201fd30b..0676a461 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -390,6 +390,7 @@ message PayAppUserInvoiceRequest { string invoice = 2; int64 amount = 3; optional string debit_npub = 4; + optional int64 fee_limit_sats = 5; } message SendAppUserToAppUserPaymentRequest { @@ -466,6 +467,7 @@ message PayInvoiceRequest{ string invoice = 1; int64 amount = 2; optional string debit_npub = 3; + optional int64 fee_limit_sats = 4; } message PayInvoiceResponse{ @@ -476,6 +478,13 @@ message PayInvoiceResponse{ int64 network_fee = 5; } +message InvoicePaymentStream { + oneof update { + Empty ack = 1; + PayInvoiceResponse done = 2; + } +} + message GetPaymentStateRequest{ string invoice = 1; diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index e64035f2..eda107e2 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -346,9 +346,9 @@ export default class { return Math.ceil(amount * this.getSettings().lndSettings.feeRateLimit + this.getSettings().lndSettings.feeFixedLimit); } - GetMaxWithinLimit(amount: number): number { - return Math.max(0, Math.floor(amount * (1 - this.getSettings().lndSettings.feeRateLimit) - this.getSettings().lndSettings.feeFixedLimit)) - } + /* GetMaxWithinLimit(amount: number): number { + return Math.max(0, Math.floor(amount * (1 - this.getSettings().lndSettings.feeRateLimit) - this.getSettings().lndSettings.feeFixedLimit)) + } */ async ChannelBalance(): Promise<{ local: number, remote: number }> { // console.log("Getting channel balance") @@ -363,7 +363,7 @@ export default class { throw new Error("lnd node is currently out of sync") } if (useProvider) { - const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from) + const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from, feeLimit) const providerDst = this.liquidProvider.GetProviderDestination() return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } } diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 5f78570e..07b84c16 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -69,14 +69,15 @@ export default class { throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing } const nostrSettings = this.settings.getSettings().nostrRelaySettings + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true) return { userId: ctx.user_id, balance: user.balance_sats, - max_withdrawable: this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true), + max_withdrawable: max, user_identifier: appUser.identifier, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: networkFeeBps, + network_max_fee_fixed: networkFeeFixed, + service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: appUser.identifier, relay: nostrSettings.relays[0] }), @@ -104,9 +105,21 @@ export default class { return this.applicationManager.PayAppUserInvoice(ctx.app_id, { amount: req.amount, invoice: req.invoice, - user_identifier: ctx.app_user_id + user_identifier: ctx.app_user_id, + debit_npub: req.debit_npub, + fee_limit_sats: req.fee_limit_sats }) } + + async PayInvoiceStream(ctx: Types.UserContext, req: Types.PayInvoiceRequest, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { + return await this.applicationManager.PayAppUserInvoiceStream(ctx.app_id, { + amount: req.amount, + invoice: req.invoice, + user_identifier: ctx.app_user_id, + debit_npub: req.debit_npub, + fee_limit_sats: req.fee_limit_sats + }, cb) + } async PayAddress(ctx: Types.UserContext, req: Types.PayInvoiceRequest): Promise { return this.applicationManager.PayAppUserInvoice(ctx.app_id, { amount: req.amount, diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 76ae76db..011533f5 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -154,17 +154,17 @@ export default class { const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }) log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString }) - + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) return { identifier: u.identifier, info: { userId: u.user.user_id, balance: u.user.balance_sats, - max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true), + max_withdrawable: max, user_identifier: u.identifier, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: networkFeeBps, + network_max_fee_fixed: networkFeeFixed, + service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }), @@ -172,7 +172,7 @@ export default class { bridge_url: this.settings.getSettings().serviceSettings.bridgeUrl }, - max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) + max_withdrawable: max } } @@ -211,16 +211,16 @@ export default class { async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) - const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true) const nostrSettings = this.settings.getSettings().nostrRelaySettings + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true) return { max_withdrawable: max, identifier: req.user_identifier, info: { userId: user.user.user_id, balance: user.user.balance_sats, - max_withdrawable: this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true), + max_withdrawable: max, user_identifier: user.identifier, - network_max_fee_bps: this.settings.getSettings().lndSettings.feeRateBps, - network_max_fee_fixed: this.settings.getSettings().lndSettings.feeFixedLimit, - service_fee_bps: this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps, + network_max_fee_bps: networkFeeBps, + network_max_fee_fixed: networkFeeFixed, + service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), ndebit: ndebitEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), nmanage: nmanageEncode({ pubkey: app.nostr_public_key!, pointer: user.identifier, relay: nostrSettings.relays[0] }), @@ -238,6 +238,12 @@ export default class { return paid } + async PayAppUserInvoiceStream(appId: string, req: Types.PayAppUserInvoiceRequest, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { + const app = await this.storage.applicationStorage.GetApplication(appId) + const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) + return this.paymentManager.PayInvoiceStream(appUser.user.user_id, req, app, cb) + } + async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const fromUser = await this.storage.applicationStorage.GetApplicationUser(app, req.from_user_identifier) diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index ac72975e..8637952c 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -50,8 +50,11 @@ export class LiquidityManager { } beforeInvoiceCreation = async (amount: number): Promise<'lnd' | 'provider'> => { - + const providerReady = this.liquidityProvider.IsReady() if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + if (!providerReady) { + throw new Error("cannot use liquidity provider, it is not ready") + } return 'provider' } @@ -78,16 +81,26 @@ export class LiquidityManager { } } - beforeOutInvoicePayment = async (amount: number): Promise<'lnd' | 'provider'> => { + beforeOutInvoicePayment = async (amount: number): Promise<{ use: 'lnd' } | { use: 'provider', feeLimit: number }> => { + const providerReady = this.liquidityProvider.IsReady() if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { - return 'provider' + if (!providerReady) { + throw new Error("cannot use liquidity provider, it is not ready") + } + const feeLimit = await this.liquidityProvider.GetExpectedFeeLimit(amount) + return { use: 'provider', feeLimit } + } + if (!providerReady) { + return { use: 'lnd' } } const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount }) - if (canHandle) { - return 'provider' + if (!canHandle) { + return { use: 'lnd' } } - return 'lnd' + const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount, canHandle) + return { use: 'provider', feeLimit } } + afterOutInvoicePaid = async () => { } shouldDrainProvider = async () => { diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 10f9bfad..0e65be90 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -28,6 +28,9 @@ export class LiquidityProvider { queue: ((state: 'ready') => void)[] = [] utils: Utils pendingPayments: Record = {} + stateCache: Types.UserInfo | null = null + unreachableSince: number | null = null + reconnecting = false incrementProviderBalance: (balance: number) => Promise // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { @@ -68,7 +71,11 @@ export class LiquidityProvider { } IsReady = () => { - return this.ready && !this.getSettings().disableLiquidityProvider + const elapsed = this.unreachableSince ? Date.now() - this.unreachableSince : 0 + if (!this.reconnecting && elapsed > 1000 * 60 * 5) { + this.GetUserState().then(() => this.reconnecting = false) + } + return this.ready && !this.getSettings().disableLiquidityProvider && !this.unreachableSince } AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { @@ -119,14 +126,29 @@ export class LiquidityProvider { if (res.status === 'ERROR') { if (res.reason !== 'timeout') { this.log("error getting user info", res.reason) + if (!this.unreachableSince) this.unreachableSince = Date.now() } return res } + this.unreachableSince = null + this.stateCache = res this.utils.stateBundler.AddBalancePoint('providerBalance', res.balance) this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable) return res } + GetFees = () => { + if (!this.stateCache) { + throw new Error("user state not cached") + } + return { + serviceFeeBps: this.stateCache.service_fee_bps, + networkFeeBps: this.stateCache.network_max_fee_bps, + networkFeeFixed: this.stateCache.network_max_fee_fixed, + + } + } + GetLatestMaxWithdrawable = async () => { if (!this.IsReady()) { return 0 @@ -163,21 +185,37 @@ export class LiquidityProvider { return serviceFee + networkFeeLimit } - CanProviderHandle = async (req: LiquidityRequest) => { + GetExpectedFeeLimit = async (amount: number) => { + if (!this.IsReady()) { + throw new Error("liquidity provider is not ready yet, disabled or unreachable") + } + const state = await this.GetUserState() + if (state.status === 'ERROR') { + throw new Error(state.reason) + } + return this.CalculateExpectedFeeLimit(amount, state) + } + + CanProviderHandle = async (req: LiquidityRequest): Promise => { if (!this.IsReady()) { return false } - const maxW = await this.GetLatestMaxWithdrawable() - if (req.action === 'spend') { - return maxW > req.amount + const state = await this.GetUserState() + if (state.status === 'ERROR') { + this.log("error getting user state", state.reason) + return false } - return true + const maxW = state.max_withdrawable + if (req.action === 'spend' && maxW < req.amount) { + return false + } + return state } AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { try { if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet or disabled") + throw new Error("liquidity provider is not ready yet, disabled or unreachable") } const res = await this.client.NewInvoice({ amountSats: amount, memo, expiry }) if (res.status === 'ERROR') { @@ -193,21 +231,43 @@ export class LiquidityProvider { } - PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => { + PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system', feeLimit?: number) => { try { if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet or disabled") + throw new Error("liquidity provider is not ready yet, disabled or unreachable") } - const userInfo = await this.GetUserState() - if (userInfo.status === 'ERROR') { - throw new Error(userInfo.reason) - } - this.pendingPayments[invoice] = decodedAmount + this.CalculateExpectedFeeLimit(decodedAmount, userInfo) - const res = await this.client.PayInvoice({ invoice, amount: 0 }) - if (res.status === 'ERROR') { + /* const userInfo = await this.GetUserState() + if (userInfo.status === 'ERROR') { + throw new Error(userInfo.reason) + } */ + const feeLimitToUse = feeLimit ? feeLimit : await this.GetExpectedFeeLimit(decodedAmount) + this.pendingPayments[invoice] = decodedAmount + feeLimitToUse //this.CalculateExpectedFeeLimit(decodedAmount, userInfo) + let acked = false + const timeout = setTimeout(() => { + this.log("10 seconds passed, still waiting for ack") + this.GetUserState() + }, 1000 * 10) + const res = await new Promise((resolve, reject) => { + this.client.PayInvoiceStream({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }, (resp) => { + if (resp.status === 'ERROR') { + this.log("error paying invoice", resp.reason) + reject(new Error(resp.reason)) + return + } + if (resp.update.type === Types.InvoicePaymentStream_update_type.ACK) { + this.log("acked") + clearTimeout(timeout) + acked = true + return + } + resolve(resp.update.done) + }) + }) + //const res = await this.client.PayInvoice({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }) + /* if (res.status === 'ERROR') { this.log("error paying invoice", res.reason) throw new Error(res.reason) - } + } */ const totalPaid = res.amount_paid + res.network_fee + res.service_fee this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) @@ -221,7 +281,7 @@ export class LiquidityProvider { GetPaymentState = async (invoice: string) => { if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet or disabled") + throw new Error("liquidity provider is not ready yet, disabled or unreachable") } const res = await this.client.GetPaymentState({ invoice }) if (res.status === 'ERROR') { @@ -233,7 +293,7 @@ export class LiquidityProvider { GetOperations = async () => { if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet or disabled") + throw new Error("liquidity provider is not ready yet, disabled or unreachable") } const res = await this.client.GetUserOperations({ latestIncomingInvoice: { ts: 0, id: 0 }, latestOutgoingInvoice: { ts: 0, id: 0 }, diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 4dcd0b6e..752c1821 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -232,14 +232,29 @@ export default class { } } - GetMaxPayableInvoice(balance: number, appUser: boolean): number { - let maxWithinServiceFee = 0 - if (appUser) { - maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) - } else { - maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee))) + GetMaxPayableInvoice(balance: number, appUser: boolean): { max: number, serviceFeeBps: number, networkFeeBps: number, networkFeeFixed: number } { + const { outgoingAppInvoiceFee, outgoingAppUserInvoiceFee, outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings + const serviceFee = appUser ? outgoingAppUserInvoiceFee : outgoingAppInvoiceFee + if (this.lnd.liquidProvider.IsReady()) { + const fees = this.lnd.liquidProvider.GetFees() + const providerServiceFee = fees.serviceFeeBps / 10000 + const providerNetworkFee = fees.networkFeeBps / 10000 + const div = 1 + serviceFee + providerServiceFee + providerNetworkFee + const max = Math.floor((balance - fees.networkFeeFixed) / div) + const networkFeeBps = fees.networkFeeBps + fees.serviceFeeBps + return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps, networkFeeFixed: fees.networkFeeFixed } } - return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) + const { feeFixedLimit, feeRateLimit, feeRateBps } = this.settings.getSettings().lndSettings + const div = 1 + serviceFee + feeRateLimit + const max = Math.floor((balance - feeFixedLimit) / div) + return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit } + /* let maxWithinServiceFee = 0 + if (appUser) { + maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) + } else { + maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee))) + } + return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) */ } async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise { const decoded = await this.lnd.DecodeInvoice(req.invoice) @@ -248,7 +263,17 @@ export default class { } } - async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise { + async PayInvoiceStream(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { + const ack = () => cb({ update: { type: Types.InvoicePaymentStream_update_type.ACK, ack: {} } }, null) + try { + const paid = await this.PayInvoice(userId, req, linkedApplication, ack) + cb({ update: { type: Types.InvoicePaymentStream_update_type.DONE, done: paid } }, null) + } catch (err: any) { + cb({ update: { type: Types.InvoicePaymentStream_update_type.ACK, ack: {} } }, err) + } + } + + async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, ack?: () => void): Promise { await this.watchDog.PaymentRequested() const maybeBanned = await this.storage.userStorage.GetUser(userId) if (maybeBanned.locked) { @@ -264,6 +289,9 @@ export default class { const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) const isAppUserPayment = userId !== linkedApplication.owner.user_id const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) + if (req.fee_limit_sats && req.fee_limit_sats < serviceFee) { + throw new Error("fee limit provided is too low to cover service fees") + } const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice) if (internalInvoice && internalInvoice.paid_at_unix > 0) { throw new Error("this invoice was already paid") @@ -276,7 +304,7 @@ export default class { if (internalInvoice) { paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub) } else { - paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, req.debit_npub) + paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount, feeLimit: req.fee_limit_sats }, linkedApplication, req.debit_npub, ack) } if (isAppUserPayment && serviceFee > 0) { await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee, "fees") @@ -292,7 +320,22 @@ export default class { } } - async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, debitNpub?: string) { + getUse = async (payAmount: number, inputLimit: number | undefined): Promise<{ use: 'lnd' | 'provider', feeLimit: number }> => { + const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) + if (use.use === 'lnd') { + const lndFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) + if (inputLimit && inputLimit < lndFeeLimit) { + this.log("WARNING requested fee limit is lower than suggested, payment might fail") + } + return { use: 'lnd', feeLimit: inputLimit || lndFeeLimit } + } + if (inputLimit && inputLimit < use.feeLimit) { + this.log("WARNING requested fee limit is lower than suggested by provider, payment might fail") + } + return { use: 'provider', feeLimit: inputLimit || use.feeLimit } + } + + async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number, feeLimit?: number }, linkedApplication: Application, debitNpub?: string, ack?: () => void) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) { throw new Error("something went wrong sending payment, please try again later") } @@ -305,16 +348,20 @@ export default class { } throw new Error("payment already in progress") } + const { amountForLnd, payAmount, serviceFee } = amounts const totalAmountToDecrement = payAmount + serviceFee - const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) - const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) + /* const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) + const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) */ + const remainingLimit = amounts.feeLimit ? amounts.feeLimit - serviceFee : undefined + const { use, feeLimit: routingFeeLimit } = await this.getUse(payAmount, remainingLimit) const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined const pendingPayment = await this.storage.StartTransaction(async tx => { await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx) return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, debitNpub) }, "payment started") this.log("ready to pay") + ack?.() try { const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index c9f92932..16051c68 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -158,6 +158,13 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.appUserManager.PayInvoice(ctx, req) }, + PayInvoiceStream: async ({ ctx, req, cb }) => { + const err = Types.PayInvoiceRequestValidate(req, { + invoice_CustomCheck: invoice => invoice !== '' + }) + if (err != null) throw new Error(err.message) + mainHandler.appUserManager.PayInvoiceStream(ctx, req, cb) + }, GetLnurlWithdrawLink: ({ ctx }) => mainHandler.paymentManager.GetLnurlWithdrawLink(ctx), GetLnurlWithdrawInfo: async ({ ctx, query }) => { if (!query.k1) { From 8e4a8b2a2af1916212d482ade3b550edf28dd66c Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 21 Nov 2025 16:45:29 +0000 Subject: [PATCH 08/29] provider: wire beacon + return balance + use cached balance --- proto/autogenerated/client.md | 14 +++ proto/autogenerated/go/types.go | 26 ++++-- proto/autogenerated/ts/types.ts | 80 +++++++++++++++++ proto/service/structs.proto | 17 ++++ src/e2e.ts | 5 +- src/index.ts | 11 ++- src/nostrMiddleware.ts | 2 +- src/services/main/appUserManager.ts | 2 +- src/services/main/applicationManager.ts | 9 +- src/services/main/debitManager.ts | 24 ++--- src/services/main/index.ts | 17 ++-- src/services/main/liquidityManager.ts | 4 +- src/services/main/liquidityProvider.ts | 111 ++++++++++++++++-------- src/services/main/paymentManager.ts | 42 ++++++--- src/services/main/rugPullTracker.ts | 2 +- src/services/nostr/handler.ts | 26 ++++-- src/services/nostr/index.ts | 7 +- 17 files changed, 307 insertions(+), 92 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 5bad6c03..11a57227 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1104,6 +1104,13 @@ The nostr server will send back a message response, and inside the body there wi - __nostr_pub__: _string_ - __user_identifier__: _string_ +### BeaconData + - __avatarUrl__: _string_ *this field is optional + - __fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional + - __name__: _string_ + - __nextRelay__: _string_ *this field is optional + - __type__: _string_ + ### BundleData - __available_chunks__: ARRAY of: _number_ - __base_64_data__: ARRAY of: _string_ @@ -1149,6 +1156,11 @@ The nostr server will send back a message response, and inside the body there wi ### CreateOneTimeInviteLinkResponse - __invitation_link__: _string_ +### CumulativeFees + - __networkFeeBps__: _number_ + - __networkFeeFixed__: _number_ + - __serviceFeeBps__: _number_ + ### DebitAuthorization - __authorized__: _boolean_ - __debit_id__: _string_ @@ -1290,6 +1302,7 @@ The nostr server will send back a message response, and inside the body there wi - __request_id__: _string_ ### LiveUserOperation + - __latest_balance__: _number_ - __operation__: _[UserOperation](#UserOperation)_ ### LndChannels @@ -1487,6 +1500,7 @@ The nostr server will send back a message response, and inside the body there wi ### PayInvoiceResponse - __amount_paid__: _number_ + - __latest_balance__: _number_ - __network_fee__: _number_ - __operation_id__: _string_ - __preimage__: _string_ diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 7a744c79..4779edde 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -177,6 +177,13 @@ type BannedAppUser struct { Nostr_pub string `json:"nostr_pub"` User_identifier string `json:"user_identifier"` } +type BeaconData struct { + Avatarurl string `json:"avatarUrl"` + Fees *CumulativeFees `json:"fees"` + Name string `json:"name"` + Nextrelay string `json:"nextRelay"` + Type string `json:"type"` +} type BundleData struct { Available_chunks []int64 `json:"available_chunks"` Base_64_data []string `json:"base_64_data"` @@ -222,6 +229,11 @@ type CreateOneTimeInviteLinkRequest struct { type CreateOneTimeInviteLinkResponse struct { Invitation_link string `json:"invitation_link"` } +type CumulativeFees struct { + Networkfeebps int64 `json:"networkFeeBps"` + Networkfeefixed int64 `json:"networkFeeFixed"` + Servicefeebps int64 `json:"serviceFeeBps"` +} type DebitAuthorization struct { Authorized bool `json:"authorized"` Debit_id string `json:"debit_id"` @@ -363,7 +375,8 @@ type LiveManageRequest struct { Request_id string `json:"request_id"` } type LiveUserOperation struct { - Operation *UserOperation `json:"operation"` + Latest_balance int64 `json:"latest_balance"` + Operation *UserOperation `json:"operation"` } type LndChannels struct { Open_channels []OpenChannel `json:"open_channels"` @@ -559,11 +572,12 @@ type PayInvoiceRequest struct { Invoice string `json:"invoice"` } type PayInvoiceResponse struct { - Amount_paid int64 `json:"amount_paid"` - Network_fee int64 `json:"network_fee"` - Operation_id string `json:"operation_id"` - Preimage string `json:"preimage"` - Service_fee int64 `json:"service_fee"` + Amount_paid int64 `json:"amount_paid"` + Latest_balance int64 `json:"latest_balance"` + Network_fee int64 `json:"network_fee"` + Operation_id string `json:"operation_id"` + Preimage string `json:"preimage"` + Service_fee int64 `json:"service_fee"` } type PayerData struct { Data map[string]string `json:"data"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 921f9a82..5e97fbef 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -983,6 +983,48 @@ export const BannedAppUserValidate = (o?: BannedAppUser, opts: BannedAppUserOpti return null } +export type BeaconData = { + avatarUrl?: string + fees?: CumulativeFees + name: string + nextRelay?: string + type: string +} +export type BeaconDataOptionalField = 'avatarUrl' | 'fees' | 'nextRelay' +export const BeaconDataOptionalFields: BeaconDataOptionalField[] = ['avatarUrl', 'fees', 'nextRelay'] +export type BeaconDataOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: BeaconDataOptionalField[] + avatarUrl_CustomCheck?: (v?: string) => boolean + fees_Options?: CumulativeFeesOptions + name_CustomCheck?: (v: string) => boolean + nextRelay_CustomCheck?: (v?: string) => boolean + type_CustomCheck?: (v: string) => boolean +} +export const BeaconDataValidate = (o?: BeaconData, opts: BeaconDataOptions = {}, path: string = 'BeaconData::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if ((o.avatarUrl || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('avatarUrl')) && typeof o.avatarUrl !== 'string') return new Error(`${path}.avatarUrl: is not a string`) + if (opts.avatarUrl_CustomCheck && !opts.avatarUrl_CustomCheck(o.avatarUrl)) return new Error(`${path}.avatarUrl: custom check failed`) + + if (typeof o.fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('fees')) { + const feesErr = CumulativeFeesValidate(o.fees, opts.fees_Options, `${path}.fees`) + if (feesErr !== null) return feesErr + } + + + if (typeof o.name !== 'string') return new Error(`${path}.name: is not a string`) + if (opts.name_CustomCheck && !opts.name_CustomCheck(o.name)) return new Error(`${path}.name: custom check failed`) + + if ((o.nextRelay || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('nextRelay')) && typeof o.nextRelay !== 'string') return new Error(`${path}.nextRelay: is not a string`) + if (opts.nextRelay_CustomCheck && !opts.nextRelay_CustomCheck(o.nextRelay)) return new Error(`${path}.nextRelay: custom check failed`) + + if (typeof o.type !== 'string') return new Error(`${path}.type: is not a string`) + if (opts.type_CustomCheck && !opts.type_CustomCheck(o.type)) return new Error(`${path}.type: custom check failed`) + + return null +} + export type BundleData = { available_chunks: number[] base_64_data: string[] @@ -1256,6 +1298,34 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL return null } +export type CumulativeFees = { + networkFeeBps: number + networkFeeFixed: number + serviceFeeBps: number +} +export const CumulativeFeesOptionalFields: [] = [] +export type CumulativeFeesOptions = OptionsBaseMessage & { + checkOptionalsAreSet?: [] + networkFeeBps_CustomCheck?: (v: number) => boolean + networkFeeFixed_CustomCheck?: (v: number) => boolean + serviceFeeBps_CustomCheck?: (v: number) => boolean +} +export const CumulativeFeesValidate = (o?: CumulativeFees, opts: CumulativeFeesOptions = {}, path: string = 'CumulativeFees::root.'): Error | null => { + if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') + if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + + if (typeof o.networkFeeBps !== 'number') return new Error(`${path}.networkFeeBps: is not a number`) + if (opts.networkFeeBps_CustomCheck && !opts.networkFeeBps_CustomCheck(o.networkFeeBps)) return new Error(`${path}.networkFeeBps: custom check failed`) + + if (typeof o.networkFeeFixed !== 'number') return new Error(`${path}.networkFeeFixed: is not a number`) + if (opts.networkFeeFixed_CustomCheck && !opts.networkFeeFixed_CustomCheck(o.networkFeeFixed)) return new Error(`${path}.networkFeeFixed: custom check failed`) + + if (typeof o.serviceFeeBps !== 'number') return new Error(`${path}.serviceFeeBps: is not a number`) + if (opts.serviceFeeBps_CustomCheck && !opts.serviceFeeBps_CustomCheck(o.serviceFeeBps)) return new Error(`${path}.serviceFeeBps: custom check failed`) + + return null +} + export type DebitAuthorization = { authorized: boolean debit_id: string @@ -2112,17 +2182,22 @@ export const LiveManageRequestValidate = (o?: LiveManageRequest, opts: LiveManag } export type LiveUserOperation = { + latest_balance: number operation: UserOperation } export const LiveUserOperationOptionalFields: [] = [] export type LiveUserOperationOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] + latest_balance_CustomCheck?: (v: number) => boolean operation_Options?: UserOperationOptions } export const LiveUserOperationValidate = (o?: LiveUserOperation, opts: LiveUserOperationOptions = {}, path: string = 'LiveUserOperation::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') + if (typeof o.latest_balance !== 'number') return new Error(`${path}.latest_balance: is not a number`) + if (opts.latest_balance_CustomCheck && !opts.latest_balance_CustomCheck(o.latest_balance)) return new Error(`${path}.latest_balance: custom check failed`) + const operationErr = UserOperationValidate(o.operation, opts.operation_Options, `${path}.operation`) if (operationErr !== null) return operationErr @@ -3287,6 +3362,7 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic export type PayInvoiceResponse = { amount_paid: number + latest_balance: number network_fee: number operation_id: string preimage: string @@ -3296,6 +3372,7 @@ export const PayInvoiceResponseOptionalFields: [] = [] export type PayInvoiceResponseOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] amount_paid_CustomCheck?: (v: number) => boolean + latest_balance_CustomCheck?: (v: number) => boolean network_fee_CustomCheck?: (v: number) => boolean operation_id_CustomCheck?: (v: string) => boolean preimage_CustomCheck?: (v: string) => boolean @@ -3308,6 +3385,9 @@ export const PayInvoiceResponseValidate = (o?: PayInvoiceResponse, opts: PayInvo if (typeof o.amount_paid !== 'number') return new Error(`${path}.amount_paid: is not a number`) if (opts.amount_paid_CustomCheck && !opts.amount_paid_CustomCheck(o.amount_paid)) return new Error(`${path}.amount_paid: custom check failed`) + if (typeof o.latest_balance !== 'number') return new Error(`${path}.latest_balance: is not a number`) + if (opts.latest_balance_CustomCheck && !opts.latest_balance_CustomCheck(o.latest_balance)) return new Error(`${path}.latest_balance: custom check failed`) + if (typeof o.network_fee !== 'number') return new Error(`${path}.network_fee: is not a number`) if (opts.network_fee_CustomCheck && !opts.network_fee_CustomCheck(o.network_fee)) return new Error(`${path}.network_fee: custom check failed`) diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 0676a461..c33e812a 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -476,6 +476,7 @@ message PayInvoiceResponse{ string operation_id = 3; int64 service_fee = 4; int64 network_fee = 5; + int64 latest_balance = 6; } message InvoicePaymentStream { @@ -613,6 +614,7 @@ message GetProductBuyLinkResponse { message LiveUserOperation { UserOperation operation = 1; + int64 latest_balance = 2; } message MigrationUpdate { optional ClosureMigration closure = 1; @@ -833,3 +835,18 @@ message MessagingToken { string device_id = 1; string firebase_messaging_token = 2; } + + +message CumulativeFees { + int64 networkFeeBps = 1; + int64 networkFeeFixed = 2; + int64 serviceFeeBps = 3; +} + +message BeaconData { + string type = 1; + string name = 2; + optional string avatarUrl = 3; + optional string nextRelay = 4; + optional CumulativeFees fees = 5; +} \ No newline at end of file diff --git a/src/e2e.ts b/src/e2e.ts index d46e2d59..5e0fda11 100644 --- a/src/e2e.ts +++ b/src/e2e.ts @@ -25,7 +25,10 @@ const start = async () => { const nostrSettings = settingsManager.getSettings().nostrRelaySettings log("initializing nostr middleware") const { Send } = nostrMiddleware(serverMethods, mainHandler, - { ...nostrSettings, apps, clients: [liquidityProviderInfo] }, + { + ...nostrSettings, apps, clients: [liquidityProviderInfo], + providerDestinationPub: settingsManager.getSettings().liquiditySettings.liquidityProviderPub + }, (e, p) => mainHandler.liquidityProvider.onEvent(e, p) ) log("starting server") diff --git a/src/index.ts b/src/index.ts index aa4c3c0b..0b53b7ce 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,10 +25,13 @@ const start = async () => { const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const serverMethods = GetServerMethods(mainHandler) log("initializing nostr middleware") - const relays = mainHandler.settings.getSettings().nostrRelaySettings.relays - const maxEventContentLength = mainHandler.settings.getSettings().nostrRelaySettings.maxEventContentLength + const relays = settingsManager.getSettings().nostrRelaySettings.relays + const maxEventContentLength = settingsManager.getSettings().nostrRelaySettings.maxEventContentLength const { Send, Stop, Ping, Reset } = nostrMiddleware(serverMethods, mainHandler, - { relays, maxEventContentLength, apps, clients: [liquidityProviderInfo] }, + { + relays, maxEventContentLength, apps, clients: [liquidityProviderInfo], + providerDestinationPub: settingsManager.getSettings().liquiditySettings.liquidityProviderPub + }, (e, p) => mainHandler.liquidityProvider.onEvent(e, p) ) exitHandler(() => { Stop(); mainHandler.Stop() }) @@ -43,7 +46,7 @@ const start = async () => { } adminManager.setAppNprofile(appNprofile) const Server = NewServer(serverMethods, serverOptions(mainHandler)) - Server.Listen(mainHandler.settings.getSettings().serviceSettings.servicePort) + Server.Listen(settingsManager.getSettings().serviceSettings.servicePort) } start() diff --git a/src/nostrMiddleware.ts b/src/nostrMiddleware.ts index cc1db630..da1172f3 100644 --- a/src/nostrMiddleware.ts +++ b/src/nostrMiddleware.ts @@ -79,7 +79,7 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett nostrTransport({ ...j, appId: event.appId }, res => { nostr.Send({ type: 'app', appId: event.appId }, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) }) }, event.startAtNano, event.startAtMs) - }) + }, beacon => mainHandler.liquidityProvider.onBeaconEvent(beacon)) // Mark nostr connected/ready after initial subscription tick mainHandler.adminManager.setNostrConnected(true) diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 07b84c16..44ee5467 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -69,7 +69,7 @@ export default class { throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing } const nostrSettings = this.settings.getSettings().nostrRelaySettings - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats, true) + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) return { userId: ctx.user_id, balance: user.balance_sats, diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 011533f5..94729dd4 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -47,12 +47,13 @@ export default class { }, 60 * 1000); // 1 minute } - async StartAppsServiceBeacon(publishBeacon: (app: Application) => void) { + async StartAppsServiceBeacon(publishBeacon: (app: Application, fees: Types.CumulativeFees) => void) { this.serviceBeaconInterval = setInterval(async () => { try { + const fees = this.paymentManager.GetAllFees() const apps = await this.storage.applicationStorage.GetApplications() apps.forEach(app => { - publishBeacon(app) + publishBeacon(app, fees) }) } catch (e) { this.log("error in beacon", (e as any).message) @@ -154,7 +155,7 @@ export default class { const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }) log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString }) - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats) return { identifier: u.identifier, info: { @@ -212,7 +213,7 @@ export default class { const app = await this.storage.applicationStorage.GetApplication(appId) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) const nostrSettings = this.settings.getSettings().nostrRelaySettings - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats, true) + const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) return { max_withdrawable: max, identifier: req.user_identifier, info: { userId: user.user.user_id, balance: user.user.balance_sats, diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 126a469c..9b4ca69f 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -9,9 +9,11 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { NostrEvent, NostrSend, SendData, SendInitiator } from '../nostr/handler.js'; import { UnsignedEvent } from 'nostr-tools'; import { Ndebit, NdebitData, NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; -import { debitAccessRulesToDebitRules, newNdebitResponse,debitRulesToDebitAccessRules, - nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, - frequencyRuleName,IntervalTypeToSeconds,unitToIntervalType } from "./debitTypes.js"; +import { + debitAccessRulesToDebitRules, newNdebitResponse, debitRulesToDebitAccessRules, + nofferErrors, AuthRequiredRes, HandleNdebitRes, expirationRuleName, + frequencyRuleName, IntervalTypeToSeconds, unitToIntervalType +} from "./debitTypes.js"; export class DebitManager { @@ -72,7 +74,7 @@ export class DebitManager { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: req.npub, id: req.request_id, appId: ctx.app_id }) return case Types.DebitResponse_response_type.INVOICE: - await this.paySingleInvoice(ctx, {invoice: req.response.invoice, npub: req.npub, request_id: req.request_id}) + await this.paySingleInvoice(ctx, { invoice: req.response.invoice, npub: req.npub, request_id: req.request_id }) return case Types.DebitResponse_response_type.AUTHORIZE: await this.handleAuthorization(ctx, req.response.authorize, { npub: req.npub, request_id: req.request_id }) @@ -82,7 +84,7 @@ export class DebitManager { } } - paySingleInvoice = async (ctx: Types.UserContext, {invoice,npub,request_id}:{invoice:string, npub:string, request_id:string}) => { + paySingleInvoice = async (ctx: Types.UserContext, { invoice, npub, request_id }: { invoice: string, npub: string, request_id: string }) => { try { this.logger("🔍 [DEBIT REQUEST] Paying single invoice") const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) @@ -97,7 +99,7 @@ export class DebitManager { } } - handleAuthorization = async (ctx: Types.UserContext,debit:Types.DebitToAuthorize, {npub,request_id}:{ npub:string, request_id:string})=>{ + handleAuthorization = async (ctx: Types.UserContext, debit: Types.DebitToAuthorize, { npub, request_id }: { npub: string, request_id: string }) => { this.logger("🔍 [DEBIT REQUEST] Handling authorization", { npub, request_id, @@ -130,7 +132,7 @@ export class DebitManager { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) throw e } - + } handleNip68Debit = async (pointerdata: NdebitData, event: NostrEvent) => { @@ -144,7 +146,7 @@ export class DebitManager { pointerdata }) const res = await this.payNdebitInvoice(event, pointerdata) - this.logger("🔍 [DEBIT REQUEST] Sending ",res.status," response") + this.logger("🔍 [DEBIT REQUEST] Sending ", res.status, " response") if (res.status === 'fail' || res.status === 'authOk') { const e = newNdebitResponse(JSON.stringify(res.debitRes), event) this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }) @@ -159,7 +161,7 @@ export class DebitManager { this.notifyPaymentSuccess(appUser, debitRes, op, event) } - handleAuthRequired = (data:NdebitData, event: NostrEvent, res: AuthRequiredRes) => { + handleAuthRequired = (data: NdebitData, event: NostrEvent, res: AuthRequiredRes) => { if (!res.appUser.nostr_public_key) { this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: event.pub, id: event.id, appId: event.appId }) return @@ -169,7 +171,9 @@ export class DebitManager { } notifyPaymentSuccess = (appUser: ApplicationUser, debitRes: NdebitSuccess, op: Types.UserOperation, event: { pub: string, id: string, appId: string }) => { - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } + const balance = appUser.user.balance_sats + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = + { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } if (appUser.nostr_public_key) { // TODO - fix before support for http streams this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key }) } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 00f65a68..d691d94c 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -103,8 +103,8 @@ export default class { } StartBeacons() { - this.applicationManager.StartAppsServiceBeacon(app => { - this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url }) + this.applicationManager.StartAppsServiceBeacon((app, fees) => { + this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, fees }) }) } @@ -373,8 +373,9 @@ export default class { getLogger({ appName: app.name })("cannot notify user, not a nostr user") return } - - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } + const balance = user.user.balance_sats + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = + { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } const j = JSON.stringify(message) this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key }) this.SendEncryptedNotification(app, user, op) @@ -396,7 +397,7 @@ export default class { }) } - async UpdateBeacon(app: Application, content: { type: 'service', name: string, avatarUrl?: string, nextRelay?: string }) { + async UpdateBeacon(app: Application, content: Types.BeaconData) { if (!app.nostr_public_key) { getLogger({ appName: app.name })("cannot update beacon, public key not set") return @@ -435,8 +436,9 @@ export default class { async ResetNostr() { const apps = await this.storage.applicationStorage.GetApplications() const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] + const fees = this.paymentManager.GetAllFees() for (const app of apps) { - await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay }) + await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay, fees }) } const defaultNames = ['wallet', 'wallet-test', this.settings.getSettings().serviceSettings.defaultAppName] @@ -453,7 +455,8 @@ export default class { apps: apps.map(a => ({ appId: a.app_id, name: a.name, privateKey: a.nostr_private_key || "", publicKey: a.nostr_public_key || "" })), relays: this.settings.getSettings().nostrRelaySettings.relays, maxEventContentLength: this.settings.getSettings().nostrRelaySettings.maxEventContentLength, - clients: [liquidityProviderInfo] + clients: [liquidityProviderInfo], + providerDestinationPub: this.settings.getSettings().liquiditySettings.liquidityProviderPub } this.nostrReset(s) } diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index 8637952c..e03b1e02 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -104,7 +104,7 @@ export class LiquidityManager { afterOutInvoicePaid = async () => { } shouldDrainProvider = async () => { - const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() + const maxW = await this.liquidityProvider.GetMaxWithdrawable() const { remote } = await this.lnd.ChannelBalance() const drainable = Math.min(maxW, remote) if (drainable < 500) { @@ -173,7 +173,7 @@ export class LiquidityManager { if (pendingChannels.pendingOpenChannels.length > 0) { return { shouldOpen: false } } - const maxW = await this.liquidityProvider.GetLatestMaxWithdrawable() + const maxW = await this.liquidityProvider.GetMaxWithdrawable() if (maxW < threshold) { return { shouldOpen: false } } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 0e65be90..54b860aa 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -1,7 +1,7 @@ import newNostrClient from '../../../proto/autogenerated/ts/nostr_client.js' import { NostrRequest } from '../../../proto/autogenerated/ts/nostr_transport.js' import * as Types from '../../../proto/autogenerated/ts/types.js' -import { getLogger } from '../helpers/logger.js' +import { ERROR, getLogger } from '../helpers/logger.js' import { Utils } from '../helpers/utilsWrapper.js' import { NostrEvent, NostrSend } from '../nostr/handler.js' import { InvoicePaidCb } from '../lnd/settings.js' @@ -9,7 +9,12 @@ import Storage from '../storage/index.js' import SettingsManager from './settingsManager.js' import { LiquiditySettings } from './settings.js' export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } - +/* export type CumulativeFees = { + networkFeeBps: number; + networkFeeFixed: number; + serviceFeeBps: number; +} +export type BeaconData = { type: 'service', name: string, avatarUrl?: string, nextRelay?: string, fees?: CumulativeFees } */ export type nostrCallback = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export class LiquidityProvider { getSettings: () => LiquiditySettings @@ -28,9 +33,11 @@ export class LiquidityProvider { queue: ((state: 'ready') => void)[] = [] utils: Utils pendingPayments: Record = {} - stateCache: Types.UserInfo | null = null - unreachableSince: number | null = null - reconnecting = false + feesCache: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } | null = null + // unreachableSince: number | null = null + // reconnecting = false + lastSeenBeacon = 0 + latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { @@ -71,11 +78,12 @@ export class LiquidityProvider { } IsReady = () => { - const elapsed = this.unreachableSince ? Date.now() - this.unreachableSince : 0 - if (!this.reconnecting && elapsed > 1000 * 60 * 5) { - this.GetUserState().then(() => this.reconnecting = false) - } - return this.ready && !this.getSettings().disableLiquidityProvider && !this.unreachableSince + /* const elapsed = this.unreachableSince ? Date.now() - this.unreachableSince : 0 + if (!this.reconnecting && elapsed > 1000 * 60 * 5) { + this.GetUserState().then(() => this.reconnecting = false) + } */ + const seenInPast2Minutes = Date.now() - this.lastSeenBeacon < 1000 * 60 * 2 + return this.ready && !this.getSettings().disableLiquidityProvider && seenInPast2Minutes } AwaitProviderReady = async (): Promise<'inactive' | 'ready'> => { @@ -114,6 +122,7 @@ export class LiquidityProvider { try { await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') this.incrementProviderBalance(res.operation.amount) + this.latestReceivedBalance = res.latest_balance } catch (err: any) { this.log("error processing incoming invoice", err.message) } @@ -126,51 +135,60 @@ export class LiquidityProvider { if (res.status === 'ERROR') { if (res.reason !== 'timeout') { this.log("error getting user info", res.reason) - if (!this.unreachableSince) this.unreachableSince = Date.now() } return res } - this.unreachableSince = null - this.stateCache = res + this.feesCache = { + networkFeeBps: res.network_max_fee_bps, + networkFeeFixed: res.network_max_fee_fixed, + serviceFeeBps: res.service_fee_bps + } + this.latestReceivedBalance = res.balance this.utils.stateBundler.AddBalancePoint('providerBalance', res.balance) this.utils.stateBundler.AddBalancePoint('providerMaxWithdrawable', res.max_withdrawable) return res } GetFees = () => { - if (!this.stateCache) { - throw new Error("user state not cached") - } - return { - serviceFeeBps: this.stateCache.service_fee_bps, - networkFeeBps: this.stateCache.network_max_fee_bps, - networkFeeFixed: this.stateCache.network_max_fee_fixed, - + if (!this.feesCache) { + throw new Error("fees not cached") } + return this.feesCache } - GetLatestMaxWithdrawable = async () => { + GetMaxWithdrawable = () => { + if (!this.IsReady() || !this.feesCache) { + return 0 + } + const { networkFeeBps, networkFeeFixed, serviceFeeBps } = this.feesCache + const totalBps = networkFeeBps + serviceFeeBps + const div = 1 + (totalBps / 10000) + return Math.floor((this.latestReceivedBalance - networkFeeFixed) / div) + } + + /* GetLatestMaxWithdrawable = async () => { + if (!this.IsReady()) { + return 0 + } + const res = await this.GetUserState() + if (res.status === 'ERROR') { + this.log("error getting user info", res.reason) + return 0 + } + return res.max_withdrawable + } */ + + GetLatestBalance = () => { if (!this.IsReady()) { return 0 } - const res = await this.GetUserState() + return this.latestReceivedBalance + /* const res = await this.GetUserState() if (res.status === 'ERROR') { this.log("error getting user info", res.reason) return 0 } - return res.max_withdrawable - } - - GetLatestBalance = async () => { - if (!this.IsReady()) { - return 0 - } - const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.balance + return res.balance */ } GetPendingBalance = async () => { @@ -270,6 +288,7 @@ export class LiquidityProvider { } */ const totalPaid = res.amount_paid + res.network_fee + res.service_fee this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) + this.latestReceivedBalance = res.latest_balance this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) return res } catch (err) { @@ -328,6 +347,26 @@ export class LiquidityProvider { this.log("configured to send to ", this.pubDestination) } } + // fees: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } + onBeaconEvent = async (beaconData: { content: string, pub: string }) => { + if (beaconData.pub !== this.pubDestination) { + this.log(ERROR, "got beacon from invalid pub", beaconData.pub, this.pubDestination) + return + } + const beacon = JSON.parse(beaconData.content) as Types.BeaconData + const err = Types.BeaconDataValidate(beacon) + if (err) { + this.log(ERROR, "error validating beacon data", err.message) + return + } + if (beacon.type !== 'service') { + this.log(ERROR, "got beacon from invalid type", beacon.type) + return + } + if (beacon.fees) { + this.feesCache = beacon.fees + } + } onEvent = async (res: { requestId: string }, fromPub: string) => { if (fromPub !== this.pubDestination) { diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 752c1821..4019167b 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -36,6 +36,8 @@ interface UserOperationInfo { }; internal?: boolean; } + + export type PendingTx = { type: 'incoming', tx: AddressReceivingTransaction } | { type: 'outgoing', tx: UserTransactionPayment } const defaultLnurlPayMetadata = (text: string) => `[["text/plain", "${text}"]]` const defaultLnAddressMetadata = (text: string, id: string) => `[["text/plain", "${text}"],["text/identifier", "${id}"]]` @@ -232,22 +234,37 @@ export default class { } } - GetMaxPayableInvoice(balance: number, appUser: boolean): { max: number, serviceFeeBps: number, networkFeeBps: number, networkFeeFixed: number } { - const { outgoingAppInvoiceFee, outgoingAppUserInvoiceFee, outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings - const serviceFee = appUser ? outgoingAppUserInvoiceFee : outgoingAppInvoiceFee + GetAllFees = (): Types.CumulativeFees => { + const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings if (this.lnd.liquidProvider.IsReady()) { const fees = this.lnd.liquidProvider.GetFees() - const providerServiceFee = fees.serviceFeeBps / 10000 - const providerNetworkFee = fees.networkFeeBps / 10000 - const div = 1 + serviceFee + providerServiceFee + providerNetworkFee - const max = Math.floor((balance - fees.networkFeeFixed) / div) const networkFeeBps = fees.networkFeeBps + fees.serviceFeeBps - return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps, networkFeeFixed: fees.networkFeeFixed } + return { networkFeeBps, networkFeeFixed: fees.networkFeeFixed, serviceFeeBps: outgoingAppUserInvoiceFeeBps } } - const { feeFixedLimit, feeRateLimit, feeRateBps } = this.settings.getSettings().lndSettings - const div = 1 + serviceFee + feeRateLimit - const max = Math.floor((balance - feeFixedLimit) / div) - return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit } + const { feeFixedLimit, feeRateBps } = this.settings.getSettings().lndSettings + return { networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps } + } + + GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } { + const { networkFeeBps, networkFeeFixed, serviceFeeBps } = this.GetAllFees() + const totalBps = networkFeeBps + serviceFeeBps + const div = 1 + (totalBps / 10000) + const max = Math.floor((balance - networkFeeFixed) / div) + return { max, serviceFeeBps, networkFeeBps, networkFeeFixed } + + /* if (this.lnd.liquidProvider.IsReady()) { + const fees = this.lnd.liquidProvider.GetFees() + const providerServiceFee = fees.serviceFeeBps / 10000 + const providerNetworkFee = fees.networkFeeBps / 10000 + const div = 1 + serviceFee + providerServiceFee + providerNetworkFee + const max = Math.floor((balance - fees.networkFeeFixed) / div) + const networkFeeBps = fees.networkFeeBps + fees.serviceFeeBps + return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps, networkFeeFixed: fees.networkFeeFixed } + } + const { feeFixedLimit, feeRateLimit, feeRateBps } = this.settings.getSettings().lndSettings + const div = 1 + serviceFee + feeRateLimit + const max = Math.floor((balance - feeFixedLimit) / div) + return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit } */ /* let maxWithinServiceFee = 0 if (appUser) { maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) @@ -317,6 +334,7 @@ export default class { operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, network_fee: paymentInfo.networkFee, service_fee: serviceFee, + latest_balance: user.balance_sats } } diff --git a/src/services/main/rugPullTracker.ts b/src/services/main/rugPullTracker.ts index ab5889a5..8feecb30 100644 --- a/src/services/main/rugPullTracker.ts +++ b/src/services/main/rugPullTracker.ts @@ -27,7 +27,7 @@ export class RugPullTracker { const providerTracker = await this.storage.liquidityStorage.GetTrackedProvider('lnPub', pubDst) const ready = this.liquidProvider.IsReady() if (ready) { - const balance = await this.liquidProvider.GetLatestBalance() + const balance = this.liquidProvider.GetLatestBalance() const pendingBalance = await this.liquidProvider.GetPendingBalance() const trackedBalance = balance + pendingBalance if (!providerTracker) { diff --git a/src/services/nostr/handler.ts b/src/services/nostr/handler.ts index e054d8de..06c83c08 100644 --- a/src/services/nostr/handler.ts +++ b/src/services/nostr/handler.ts @@ -2,7 +2,7 @@ import WebSocket from 'ws' Object.assign(global, { WebSocket: WebSocket }); import crypto from 'crypto' -import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44 } from 'nostr-tools' +import { SimplePool, Event, UnsignedEvent, finalizeEvent, Relay, nip44, Filter } from 'nostr-tools' import { ERROR, getLogger } from '../helpers/logger.js' import { nip19 } from 'nostr-tools' import { encrypt as encryptV1, decrypt as decryptV1, getSharedSecret as getConversationKeyV1 } from './nip44v1.js' @@ -26,6 +26,7 @@ export type NostrSettings = { relays: string[] clients: ClientInfo[] maxEventContentLength: number + providerDestinationPub: string } export type NostrEvent = { @@ -69,9 +70,14 @@ type ProcessMetricsResponse = { type: 'processMetrics' metrics: ProcessMetrics } +type BeaconResponse = { + type: 'beacon' + content: string + pub: string +} export type ChildProcessRequest = SettingsRequest | SendRequest | PingRequest -export type ChildProcessResponse = ReadyResponse | EventResponse | ProcessMetricsResponse | PingResponse +export type ChildProcessResponse = ReadyResponse | EventResponse | ProcessMetricsResponse | PingResponse | BeaconResponse const send = (message: ChildProcessResponse) => { if (process.send) { process.send(message, undefined, undefined, err => { @@ -218,18 +224,28 @@ export default class Handler { appIds: appIds, listeningForPubkeys: appIds }) - - return relay.subscribe([ + const subs: Filter[] = [ { since: Math.ceil(Date.now() / 1000), kinds: supportedKinds, '#p': appIds, } - ], { + ] + if (this.settings.providerDestinationPub) { + subs.push({ + kinds: [30078], '#d': ['Lightning.Pub'], + authors: [this.settings.providerDestinationPub] + }) + } + return relay.subscribe(subs, { oneose: () => { this.log("up to date with nostr events") }, onevent: async (e) => { + if (e.kind === 30078 && e.pubkey === this.settings.providerDestinationPub) { + send({ type: 'beacon', content: e.content, pub: e.pubkey }) + return + } if (!supportedKinds.includes(e.kind) || !e.pubkey) { return } diff --git a/src/services/nostr/index.ts b/src/services/nostr/index.ts index 50fdf61b..68773ee5 100644 --- a/src/services/nostr/index.ts +++ b/src/services/nostr/index.ts @@ -3,7 +3,7 @@ import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, S import { Utils } from '../helpers/utilsWrapper.js' import { getLogger, ERROR } from '../helpers/logger.js' type EventCallback = (event: NostrEvent) => void - +type BeaconCallback = (beacon: { content: string, pub: string }) => void @@ -13,7 +13,7 @@ export default class NostrSubprocess { utils: Utils awaitingPongs: (() => void)[] = [] log = getLogger({}) - constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback) { + constructor(settings: NostrSettings, utils: Utils, eventCallback: EventCallback, beaconCallback: BeaconCallback) { this.utils = utils this.childProcess = fork("./build/src/services/nostr/handler") this.childProcess.on("error", (error) => { @@ -43,6 +43,9 @@ export default class NostrSubprocess { this.awaitingPongs.forEach(resolve => resolve()) this.awaitingPongs = [] break + case 'beacon': + beaconCallback({ content: message.content, pub: message.pub }) + break default: console.error("unknown nostr event response", message) break; From bc7d586c8a300bcf5df400b1e44005303f0edbb1 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 21 Nov 2025 17:13:00 +0000 Subject: [PATCH 09/29] fix --- src/services/storage/tlv/tlvFilesStorageFactory.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/services/storage/tlv/tlvFilesStorageFactory.ts b/src/services/storage/tlv/tlvFilesStorageFactory.ts index 6f8bbb51..625f21bd 100644 --- a/src/services/storage/tlv/tlvFilesStorageFactory.ts +++ b/src/services/storage/tlv/tlvFilesStorageFactory.ts @@ -19,7 +19,7 @@ export class TlvStorageFactory extends EventEmitter { private debug: boolean = false; private _nostrSend: NostrSend = () => { throw new Error('nostr send not initialized yet') } private allowResetMetricsStorages: boolean - log = getLogger({component: 'TlvStorageFactory'}) + log = getLogger({ component: 'TlvStorageFactory' }) constructor(allowResetMetricsStorages: boolean) { super(); this.allowResetMetricsStorages = allowResetMetricsStorages @@ -134,10 +134,15 @@ export class TlvStorageFactory extends EventEmitter { return this.handleOp(op) } - ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise { + async ProcessMetrics(metrics: ProcessMetrics, processName: string): Promise { const opId = Math.random().toString() const op: ProcessMetricsTlvOperation = { type: 'processMetrics', opId, metrics, processName } - return this.handleOp(op) + try { + return this.handleOp(op) + } catch (error: any) { + this.log(ERROR, 'Error processing metrics', error.message) + } + return } From 887b11c70083cd90f68124b03053d503c3f14316 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 21 Nov 2025 17:50:31 +0000 Subject: [PATCH 10/29] deb --- src/services/main/liquidityProvider.ts | 7 ++++--- src/tests/setupBootstrapped.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 54b860aa..e34499a4 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -39,6 +39,7 @@ export class LiquidityProvider { lastSeenBeacon = 0 latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise + rand = Math.random() // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { this.utils = utils @@ -327,7 +328,7 @@ export class LiquidityProvider { } setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => { - this.log("setting nostr info") + this.log("setting nostr info", this.rand) this.clientId = clientId this.myPub = myPub this.setSetIfConfigured() @@ -336,7 +337,7 @@ export class LiquidityProvider { attachNostrSend(f: NostrSend) { - this.log("attaching nostrSend action") + this.log("attaching nostrSend action", this.rand) this.nostrSend = f this.setSetIfConfigured() } @@ -344,7 +345,7 @@ export class LiquidityProvider { setSetIfConfigured = () => { if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) { this.configured = true - this.log("configured to send to ", this.pubDestination) + this.log("configured to send to ", this.pubDestination, this.rand) } } // fees: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } diff --git a/src/tests/setupBootstrapped.ts b/src/tests/setupBootstrapped.ts index dfcf16c7..9ea29d91 100644 --- a/src/tests/setupBootstrapped.ts +++ b/src/tests/setupBootstrapped.ts @@ -46,6 +46,7 @@ export const initBootstrappedInstance = async (T: TestBase) => { await new Promise(res => { const interval = setInterval(async () => { const canHandle = await bootstrapped.liquidityProvider.CanProviderHandle({ action: 'receive', amount: 2000 }) + console.log("can handle", canHandle) if (canHandle) { clearInterval(interval) res() From 3533e0e206aeb52a8754e9bc85ecf12a95347fb1 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 21 Nov 2025 18:04:42 +0000 Subject: [PATCH 11/29] deb --- src/services/main/liquidityProvider.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index e34499a4..eef746da 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -217,6 +217,7 @@ export class LiquidityProvider { CanProviderHandle = async (req: LiquidityRequest): Promise => { if (!this.IsReady()) { + this.log("provider is not ready", this.rand) return false } const state = await this.GetUserState() From 21581e3bb73e1f3295a2490986c15c30c5119f1f Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 21 Nov 2025 18:18:34 +0000 Subject: [PATCH 12/29] deb --- src/services/main/liquidityProvider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index eef746da..2fb054dd 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -109,7 +109,7 @@ export class LiquidityProvider { if (res.status === 'ERROR' && res.reason !== 'timeout') { return } - this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0) + this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0, this.rand) this.ready = true this.queue.forEach(q => q('ready')) this.log("subbing to user operations") From e4033d4159422965bfdd3213c3a667a17e2569fa Mon Sep 17 00:00:00 2001 From: boufni95 Date: Fri, 21 Nov 2025 18:24:33 +0000 Subject: [PATCH 13/29] fix --- src/services/main/liquidityProvider.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 2fb054dd..7335b78c 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -39,7 +39,7 @@ export class LiquidityProvider { lastSeenBeacon = 0 latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise - rand = Math.random() + // rand = Math.random() // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { this.utils = utils @@ -109,7 +109,8 @@ export class LiquidityProvider { if (res.status === 'ERROR' && res.reason !== 'timeout') { return } - this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0, this.rand) + this.log("provider ready with balance:", res.status === 'OK' ? res.balance : 0) + this.lastSeenBeacon = Date.now() this.ready = true this.queue.forEach(q => q('ready')) this.log("subbing to user operations") @@ -217,7 +218,7 @@ export class LiquidityProvider { CanProviderHandle = async (req: LiquidityRequest): Promise => { if (!this.IsReady()) { - this.log("provider is not ready", this.rand) + this.log("provider is not ready") return false } const state = await this.GetUserState() @@ -329,7 +330,7 @@ export class LiquidityProvider { } setNostrInfo = ({ clientId, myPub }: { myPub: string, clientId: string }) => { - this.log("setting nostr info", this.rand) + this.log("setting nostr info") this.clientId = clientId this.myPub = myPub this.setSetIfConfigured() @@ -338,7 +339,7 @@ export class LiquidityProvider { attachNostrSend(f: NostrSend) { - this.log("attaching nostrSend action", this.rand) + this.log("attaching nostrSend action") this.nostrSend = f this.setSetIfConfigured() } @@ -346,7 +347,7 @@ export class LiquidityProvider { setSetIfConfigured = () => { if (this.nostrSend && !!this.pubDestination && !!this.clientId && !!this.myPub) { this.configured = true - this.log("configured to send to ", this.pubDestination, this.rand) + this.log("configured to send to ") } } // fees: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } From f7c26ee38a72e7efe08bae122b1327cbe3df7faa Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 24 Nov 2025 18:41:19 +0000 Subject: [PATCH 14/29] fix payment stream --- proto/autogenerated/client.md | 15 --- proto/autogenerated/go/http_client.go | 2 - proto/autogenerated/go/types.go | 15 --- proto/autogenerated/ts/http_client.ts | 1 - proto/autogenerated/ts/nostr_client.ts | 16 ---- proto/autogenerated/ts/nostr_transport.ts | 16 ---- proto/autogenerated/ts/types.ts | 60 ------------ proto/service/methods.proto | 7 -- proto/service/structs.proto | 7 -- src/services/main/appUserManager.ts | 17 ---- src/services/main/applicationManager.ts | 30 ++++-- src/services/main/debitManager.ts | 46 +++------- src/services/main/debitTypes.ts | 6 +- src/services/main/index.ts | 1 + src/services/main/liquidityProvider.ts | 106 +++++----------------- src/services/main/paymentManager.ts | 65 ++++++------- src/services/serverMethods/index.ts | 7 -- 17 files changed, 90 insertions(+), 327 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 11a57227..42f5621b 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -275,11 +275,6 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayInvoiceRequest](#PayInvoiceRequest) - output: [PayInvoiceResponse](#PayInvoiceResponse) -- PayInvoiceStream - - auth type: __User__ - - input: [PayInvoiceRequest](#PayInvoiceRequest) - - output: [InvoicePaymentStream](#InvoicePaymentStream) - - PingSubProcesses - auth type: __Metrics__ - This methods has an __empty__ __request__ body @@ -865,13 +860,6 @@ The nostr server will send back a message response, and inside the body there wi - input: [PayInvoiceRequest](#PayInvoiceRequest) - output: [PayInvoiceResponse](#PayInvoiceResponse) -- PayInvoiceStream - - auth type: __User__ - - http method: __post__ - - http route: __/api/user/invoice/pay/stream__ - - input: [PayInvoiceRequest](#PayInvoiceRequest) - - output: [InvoicePaymentStream](#InvoicePaymentStream) - - PingSubProcesses - auth type: __Metrics__ - http method: __post__ @@ -1280,9 +1268,6 @@ The nostr server will send back a message response, and inside the body there wi - __token__: _string_ - __url__: _string_ -### InvoicePaymentStream - - __update__: _[InvoicePaymentStream_update](#InvoicePaymentStream_update)_ - ### LatestBundleMetricReq - __limit__: _number_ *this field is optional diff --git a/proto/autogenerated/go/http_client.go b/proto/autogenerated/go/http_client.go index 7072488f..8c20a0a6 100644 --- a/proto/autogenerated/go/http_client.go +++ b/proto/autogenerated/go/http_client.go @@ -121,7 +121,6 @@ type Client struct { PayAddress func(req PayAddressRequest) (*PayAddressResponse, error) PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error) PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error) - PayInvoiceStream func(req PayInvoiceRequest) (*InvoicePaymentStream, error) PingSubProcesses func() error RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) ResetDebit func(req DebitOperation) error @@ -1836,7 +1835,6 @@ func NewClient(params ClientParams) *Client { } return &res, nil }, - // server streaming method: PayInvoiceStream not implemented PingSubProcesses: func() error { auth, err := params.RetrieveMetricsAuth() if err != nil { diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 4779edde..5b9fb05d 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -353,9 +353,6 @@ type HttpCreds struct { Token string `json:"token"` Url string `json:"url"` } -type InvoicePaymentStream struct { - Update *InvoicePaymentStream_update `json:"update"` -} type LatestBundleMetricReq struct { Limit int64 `json:"limit"` } @@ -770,18 +767,6 @@ type DebitRule_rule struct { Expiration_rule *DebitExpirationRule `json:"expiration_rule"` Frequency_rule *FrequencyRule `json:"frequency_rule"` } -type InvoicePaymentStream_update_type string - -const ( - ACK InvoicePaymentStream_update_type = "ack" - DONE InvoicePaymentStream_update_type = "done" -) - -type InvoicePaymentStream_update struct { - Type InvoicePaymentStream_update_type `json:"type"` - Ack *Empty `json:"ack"` - Done *PayInvoiceResponse `json:"done"` -} type LiveDebitRequest_debit_type string const ( diff --git a/proto/autogenerated/ts/http_client.ts b/proto/autogenerated/ts/http_client.ts index 66f27b92..9c37e1b3 100644 --- a/proto/autogenerated/ts/http_client.ts +++ b/proto/autogenerated/ts/http_client.ts @@ -881,7 +881,6 @@ export default (params: ClientParams) => ({ } return { status: 'ERROR', reason: 'invalid response' } }, - PayInvoiceStream: async (request: Types.PayInvoiceRequest, cb: (v:ResultError | ({ status: 'OK' }& Types.InvoicePaymentStream)) => void): Promise => { throw new Error('http streams are not supported')}, PingSubProcesses: async (): Promise => { const auth = await params.retrieveMetricsAuth() if (auth === null) throw new Error('retrieveMetricsAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_client.ts b/proto/autogenerated/ts/nostr_client.ts index 002561bb..38795d01 100644 --- a/proto/autogenerated/ts/nostr_client.ts +++ b/proto/autogenerated/ts/nostr_client.ts @@ -755,22 +755,6 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ } return { status: 'ERROR', reason: 'invalid response' } }, - PayInvoiceStream: async (request: Types.PayInvoiceRequest, cb: (res:ResultError | ({ status: 'OK' }& Types.InvoicePaymentStream)) => void): Promise => { - const auth = await params.retrieveNostrUserAuth() - if (auth === null) throw new Error('retrieveNostrUserAuth() returned null') - const nostrRequest: NostrRequest = {} - nostrRequest.body = request - subscribe(params.pubDestination, {rpcName:'PayInvoiceStream',authIdentifier:auth, ...nostrRequest }, (data) => { - if (data.status === 'ERROR' && typeof data.reason === 'string') return cb(data) - if (data.status === 'OK') { - const result = data - if(!params.checkResult) return cb({ status: 'OK', ...result }) - const error = Types.InvoicePaymentStreamValidate(result) - if (error === null) { return cb({ status: 'OK', ...result }) } else return cb({ status: 'ERROR', reason: error.message }) - } - return cb({ status: 'ERROR', reason: 'invalid response' }) - }) - }, PingSubProcesses: async (): Promise => { const auth = await params.retrieveNostrMetricsAuth() if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') diff --git a/proto/autogenerated/ts/nostr_transport.ts b/proto/autogenerated/ts/nostr_transport.ts index c0d08bd0..cd9e708f 100644 --- a/proto/autogenerated/ts/nostr_transport.ts +++ b/proto/autogenerated/ts/nostr_transport.ts @@ -1190,22 +1190,6 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => { opts.metricsCallback([{ ...info, ...stats, ...authContext }]) }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } break - case 'PayInvoiceStream': - try { - if (!methods.PayInvoiceStream) throw new Error('method: PayInvoiceStream is not implemented') - const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier) - stats.guard = process.hrtime.bigint() - authCtx = authContext - const request = req.body - const error = Types.PayInvoiceRequestValidate(request) - stats.validate = process.hrtime.bigint() - if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback) - methods.PayInvoiceStream({rpcName:'PayInvoiceStream', ctx:authContext , req: request ,cb: (response, err) => { - stats.handle = process.hrtime.bigint() - if (err) { logErrorAndReturnResponse(err, err.message, res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)} else { res({status: 'OK', ...response});opts.metricsCallback([{ ...info, ...stats, ...authContext }])} - }}) - }catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e } - break case 'PingSubProcesses': try { if (!methods.PingSubProcesses) throw new Error('method: PingSubProcesses is not implemented') diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 5e97fbef..f4d1764b 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -262,9 +262,6 @@ export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvo export type PayInvoice_Input = {rpcName:'PayInvoice', req: PayInvoiceRequest} export type PayInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse) -export type PayInvoiceStream_Input = {rpcName:'PayInvoiceStream', req: PayInvoiceRequest, cb:(res: InvoicePaymentStream, err:Error|null)=> void} -export type PayInvoiceStream_Output = ResultError | { status: 'OK' } - export type PingSubProcesses_Input = {rpcName:'PingSubProcesses'} export type PingSubProcesses_Output = ResultError | { status: 'OK' } @@ -392,7 +389,6 @@ export type ServerMethods = { PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise - PayInvoiceStream?: (req: PayInvoiceStream_Input & {ctx: UserContext }) => Promise PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise ResetDebit?: (req: ResetDebit_Input & {ctx: UserContext }) => Promise @@ -2054,25 +2050,6 @@ export const HttpCredsValidate = (o?: HttpCreds, opts: HttpCredsOptions = {}, pa return null } -export type InvoicePaymentStream = { - update: InvoicePaymentStream_update -} -export const InvoicePaymentStreamOptionalFields: [] = [] -export type InvoicePaymentStreamOptions = OptionsBaseMessage & { - checkOptionalsAreSet?: [] - update_Options?: InvoicePaymentStream_updateOptions -} -export const InvoicePaymentStreamValidate = (o?: InvoicePaymentStream, opts: InvoicePaymentStreamOptions = {}, path: string = 'InvoicePaymentStream::root.'): Error | null => { - if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') - if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') - - const updateErr = InvoicePaymentStream_updateValidate(o.update, opts.update_Options, `${path}.update`) - if (updateErr !== null) return updateErr - - - return null -} - export type LatestBundleMetricReq = { limit?: number } @@ -4435,43 +4412,6 @@ export const DebitRule_ruleValidate = (o?: DebitRule_rule, opts:DebitRule_ruleOp if (frequency_ruleErr !== null) return frequency_ruleErr - break - default: - return new Error(path + ': unknown type '+ stringType) - } - return null -} -export enum InvoicePaymentStream_update_type { - ACK = 'ack', - DONE = 'done', -} -export const enumCheckInvoicePaymentStream_update_type = (e?: InvoicePaymentStream_update_type): boolean => { - for (const v in InvoicePaymentStream_update_type) if (e === v) return true - return false -} -export type InvoicePaymentStream_update = - {type:InvoicePaymentStream_update_type.ACK, ack:Empty}| - {type:InvoicePaymentStream_update_type.DONE, done:PayInvoiceResponse} - -export type InvoicePaymentStream_updateOptions = { - ack_Options?: EmptyOptions - done_Options?: PayInvoiceResponseOptions -} -export const InvoicePaymentStream_updateValidate = (o?: InvoicePaymentStream_update, opts:InvoicePaymentStream_updateOptions = {}, path: string = 'InvoicePaymentStream_update::root.'): Error | null => { - if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') - const stringType: string = o.type - switch (o.type) { - case InvoicePaymentStream_update_type.ACK: - const ackErr = EmptyValidate(o.ack, opts.ack_Options, `${path}.ack`) - if (ackErr !== null) return ackErr - - - break - case InvoicePaymentStream_update_type.DONE: - const doneErr = PayInvoiceResponseValidate(o.done, opts.done_Options, `${path}.done`) - if (doneErr !== null) return doneErr - - break default: return new Error(path + ': unknown type '+ stringType) diff --git a/proto/service/methods.proto b/proto/service/methods.proto index eb253572..4cff7379 100644 --- a/proto/service/methods.proto +++ b/proto/service/methods.proto @@ -517,13 +517,6 @@ service LightningPub { option (nostr) = true; } - rpc PayInvoiceStream(structs.PayInvoiceRequest) returns (stream structs.InvoicePaymentStream){ - option (auth_type) = "User"; - option (http_method) = "post"; - option (http_route) = "/api/user/invoice/pay/stream"; - option (nostr) = true; - } - rpc GetPaymentState(structs.GetPaymentStateRequest) returns (structs.PaymentState){ option (auth_type) = "User"; option (http_method) = "post"; diff --git a/proto/service/structs.proto b/proto/service/structs.proto index c33e812a..78746db4 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -479,13 +479,6 @@ message PayInvoiceResponse{ int64 latest_balance = 6; } -message InvoicePaymentStream { - oneof update { - Empty ack = 1; - PayInvoiceResponse done = 2; - } -} - message GetPaymentStateRequest{ string invoice = 1; diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 44ee5467..f8d41b16 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -111,23 +111,6 @@ export default class { }) } - async PayInvoiceStream(ctx: Types.UserContext, req: Types.PayInvoiceRequest, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { - return await this.applicationManager.PayAppUserInvoiceStream(ctx.app_id, { - amount: req.amount, - invoice: req.invoice, - user_identifier: ctx.app_user_id, - debit_npub: req.debit_npub, - fee_limit_sats: req.fee_limit_sats - }, cb) - } - async PayAddress(ctx: Types.UserContext, req: Types.PayInvoiceRequest): Promise { - return this.applicationManager.PayAppUserInvoice(ctx.app_id, { - amount: req.amount, - invoice: req.invoice, - user_identifier: ctx.app_user_id - }) - } - async EnrollMessagingToken(ctx: Types.UserContext, req: Types.MessagingToken): Promise { const app = await this.storage.applicationStorage.GetApplication(ctx.app_id); const user = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id); diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 94729dd4..d2cc56fc 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -10,6 +10,7 @@ import { Application } from '../storage/entity/Application.js' import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' import SettingsManager from './settingsManager.js' +import { NostrSend, SendData, SendInitiator } from '../nostr/handler.js' const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds type NsecLinkingData = { @@ -17,7 +18,7 @@ type NsecLinkingData = { expiry: number } export default class { - + _nostrSend: NostrSend | null = null storage: Storage settings: SettingsManager paymentManager: PaymentManager @@ -33,6 +34,17 @@ export default class { this.StartLinkingTokenInterval() } + attachNostrSend = (nostrSend: NostrSend) => { + this._nostrSend = nostrSend + } + + nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { + if (!this._nostrSend) { + throw new Error("No nostrSend attached") + } + this._nostrSend(initiator, data, relays) + } + StartLinkingTokenInterval() { this.linkingTokenInterval = setInterval(() => { const now = Date.now(); @@ -234,15 +246,21 @@ export default class { async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise { const app = await this.storage.applicationStorage.GetApplication(appId) const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) - const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app) + const paid = await this.paymentManager.PayInvoice(appUser.user.user_id, req, app, pendingOp => { + this.notifyAppUserPayment(appUser, pendingOp) + }) + this.notifyAppUserPayment(appUser, paid.operation) getLogger({ appName: app.name })(appUser.identifier, "invoice paid", paid.amount_paid, "sats") return paid } - async PayAppUserInvoiceStream(appId: string, req: Types.PayAppUserInvoiceRequest, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { - const app = await this.storage.applicationStorage.GetApplication(appId) - const appUser = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) - return this.paymentManager.PayInvoiceStream(appUser.user.user_id, req, app, cb) + notifyAppUserPayment = (appUser: ApplicationUser, op: Types.UserOperation) => { + const balance = appUser.user.balance_sats + const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = + { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } + if (appUser.nostr_public_key) { // TODO - fix before support for http streams + this.nostrSend({ type: 'app', appId: appUser.application.app_id }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key }) + } } async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise { diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 9b4ca69f..11270629 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -34,6 +34,7 @@ export class DebitManager { attachNostrSend = (nostrSend: NostrSend) => { this._nostrSend = nostrSend } + nostrSend: NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => { if (!this._nostrSend) { throw new Error("No nostrSend attached") @@ -87,11 +88,9 @@ export class DebitManager { paySingleInvoice = async (ctx: Types.UserContext, { invoice, npub, request_id }: { invoice: string, npub: string, request_id: string }) => { try { this.logger("🔍 [DEBIT REQUEST] Paying single invoice") - const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) - const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) - const { op, payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) + const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } - this.notifyPaymentSuccess(appUser, debitRes, op, { appId: ctx.app_id, pub: npub, id: request_id }) + this.notifyPaymentSuccess(debitRes, { appId: ctx.app_id, pub: npub, id: request_id }) } catch (e: any) { this.logger("❌ [DEBIT REQUEST] Error in single invoice payment") this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) @@ -124,9 +123,9 @@ export class DebitManager { const appUser = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id) this.validateAccessRules(access, app, appUser) this.logger("🔍 [DEBIT REQUEST] Sending debit payment") - const { op, payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) + const { payment } = await this.sendDebitPayment(ctx.app_id, ctx.app_user_id, npub, invoice) const debitRes: NdebitSuccess = { res: 'ok', preimage: payment.preimage } - this.notifyPaymentSuccess(appUser, debitRes, op, { appId: ctx.app_id, pub: npub, id: request_id }) + this.notifyPaymentSuccess(debitRes, { appId: ctx.app_id, pub: npub, id: request_id }) } catch (e: any) { this.logger("❌ [DEBIT REQUEST] Error in debit authorization") this.sendDebitResponse({ res: 'GFY', error: nofferErrors[1], code: 1 }, { pub: npub, id: request_id, appId: ctx.app_id }) @@ -157,8 +156,8 @@ export class DebitManager { this.handleAuthRequired(pointerdata, event, res) return } - const { op, debitRes } = res - this.notifyPaymentSuccess(appUser, debitRes, op, event) + const { debitRes } = res + this.notifyPaymentSuccess(debitRes, event) } handleAuthRequired = (data: NdebitData, event: NostrEvent, res: AuthRequiredRes) => { @@ -170,13 +169,7 @@ export class DebitManager { this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: res.appUser.nostr_public_key }) } - notifyPaymentSuccess = (appUser: ApplicationUser, debitRes: NdebitSuccess, op: Types.UserOperation, event: { pub: string, id: string, appId: string }) => { - const balance = appUser.user.balance_sats - const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = - { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } - if (appUser.nostr_public_key) { // TODO - fix before support for http streams - this.nostrSend({ type: 'app', appId: event.appId }, { type: 'content', content: JSON.stringify(message), pub: appUser.nostr_public_key }) - } + notifyPaymentSuccess = (debitRes: NdebitSuccess, event: { pub: string, id: string, appId: string }) => { this.sendDebitResponse(debitRes, event) } @@ -290,15 +283,14 @@ export class DebitManager { } await this.validateAccessRules(authorization, app, appUser) this.logger("🔍 [DEBIT REQUEST] Sending requested debit payment") - const { op, payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) - return { status: 'invoicePaid', op, app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } + const { payment } = await this.sendDebitPayment(appId, appUserId, requestorPub, bolt11) + return { status: 'invoicePaid', app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } } } sendDebitPayment = async (appId: string, appUserId: string, requestorPub: string, bolt11: string) => { const payment = await this.applicationManager.PayAppUserInvoice(appId, { amount: 0, invoice: bolt11, user_identifier: appUserId, debit_npub: requestorPub }) await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee + payment.network_fee) - const op = this.newPaymentOperation(payment, bolt11) - return { payment, op } + return { payment } } validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise => { @@ -329,21 +321,5 @@ export class DebitManager { } return true } - - newPaymentOperation = (payment: Types.PayInvoiceResponse, bolt11: string) => { - return { - amount: payment.amount_paid, - paidAtUnix: Math.floor(Date.now() / 1000), - inbound: false, - type: Types.UserOperationType.OUTGOING_INVOICE, - identifier: bolt11, - operationId: payment.operation_id, - network_fee: payment.network_fee, - service_fee: payment.service_fee, - confirmed: true, - tx_hash: "", - internal: payment.network_fee === 0 - } - } } diff --git a/src/services/main/debitTypes.ts b/src/services/main/debitTypes.ts index 83aca293..719de95f 100644 --- a/src/services/main/debitTypes.ts +++ b/src/services/main/debitTypes.ts @@ -1,9 +1,9 @@ import * as Types from "../../../proto/autogenerated/ts/types.js"; -import { DebitAccessRules } from '../storage/entity/DebitAccess.js'; +import { DebitAccessRules } from '../storage/entity/DebitAccess.js'; import { Application } from '../storage/entity/Application.js'; import { ApplicationUser } from '../storage/entity/ApplicationUser.js'; import { UnsignedEvent } from 'nostr-tools'; -import { NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; +import { NdebitFailure, NdebitSuccess, RecurringDebitTimeUnit } from "@shocknet/clink-sdk"; export const expirationRuleName = 'expiration' export const frequencyRuleName = 'frequency' @@ -96,7 +96,7 @@ export const nofferErrors = { } export type AuthRequiredRes = { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, app: Application, appUser: ApplicationUser } export type HandleNdebitRes = { status: 'fail', debitRes: NdebitFailure } - | { status: 'invoicePaid', op: Types.UserOperation, app: Application, appUser: ApplicationUser, debitRes: NdebitSuccess } + | { status: 'invoicePaid', app: Application, appUser: ApplicationUser, debitRes: NdebitSuccess } | AuthRequiredRes | { status: 'authOk', debitRes: NdebitSuccess } diff --git a/src/services/main/index.ts b/src/services/main/index.ts index d691d94c..2afe09ff 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -115,6 +115,7 @@ export default class { this.offerManager.attachNostrSend(f) this.managementManager.attachNostrSend(f) this.utils.attachNostrSend(f) + this.applicationManager.attachNostrSend(f) //this.webRTC.attachNostrSend(f) } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 7335b78c..43f20bdc 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -9,12 +9,6 @@ import Storage from '../storage/index.js' import SettingsManager from './settingsManager.js' import { LiquiditySettings } from './settings.js' export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } -/* export type CumulativeFees = { - networkFeeBps: number; - networkFeeFixed: number; - serviceFeeBps: number; -} -export type BeaconData = { type: 'service', name: string, avatarUrl?: string, nextRelay?: string, fees?: CumulativeFees } */ export type nostrCallback = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export class LiquidityProvider { getSettings: () => LiquiditySettings @@ -34,12 +28,10 @@ export class LiquidityProvider { utils: Utils pendingPayments: Record = {} feesCache: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } | null = null - // unreachableSince: number | null = null - // reconnecting = false lastSeenBeacon = 0 latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise - // rand = Math.random() + pendingPaymentsAck: Record = {} // make the sub process accept client constructor(getSettings: () => LiquiditySettings, utils: Utils, invoicePaidCb: InvoicePaidCb, incrementProviderBalance: (balance: number) => Promise) { this.utils = utils @@ -79,10 +71,6 @@ export class LiquidityProvider { } IsReady = () => { - /* const elapsed = this.unreachableSince ? Date.now() - this.unreachableSince : 0 - if (!this.reconnecting && elapsed > 1000 * 60 * 5) { - this.GetUserState().then(() => this.reconnecting = false) - } */ const seenInPast2Minutes = Date.now() - this.lastSeenBeacon < 1000 * 60 * 2 return this.ready && !this.getSettings().disableLiquidityProvider && seenInPast2Minutes } @@ -125,6 +113,9 @@ export class LiquidityProvider { await this.invoicePaidCb(res.operation.identifier, res.operation.amount, 'provider') this.incrementProviderBalance(res.operation.amount) this.latestReceivedBalance = res.latest_balance + if (!res.operation.inbound && !res.operation.confirmed) { + delete this.pendingPaymentsAck[res.operation.identifier] + } } catch (err: any) { this.log("error processing incoming invoice", err.message) } @@ -168,69 +159,36 @@ export class LiquidityProvider { return Math.floor((this.latestReceivedBalance - networkFeeFixed) / div) } - /* GetLatestMaxWithdrawable = async () => { - if (!this.IsReady()) { - return 0 - } - const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.max_withdrawable - } */ - GetLatestBalance = () => { if (!this.IsReady()) { return 0 } return this.latestReceivedBalance - /* const res = await this.GetUserState() - if (res.status === 'ERROR') { - this.log("error getting user info", res.reason) - return 0 - } - return res.balance */ } GetPendingBalance = async () => { return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0) } - CalculateExpectedFeeLimit = (amount: number, info: Types.UserInfo) => { - const serviceFeeRate = info.service_fee_bps / 10000 + CalculateExpectedFeeLimit = (amount: number) => { + const fees = this.GetFees() + const serviceFeeRate = fees.serviceFeeBps / 10000 const serviceFee = Math.ceil(serviceFeeRate * amount) - const networkMaxFeeRate = info.network_max_fee_bps / 10000 - const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + info.network_max_fee_fixed) + const networkMaxFeeRate = fees.networkFeeBps / 10000 + const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + fees.networkFeeFixed) return serviceFee + networkFeeLimit } - GetExpectedFeeLimit = async (amount: number) => { - if (!this.IsReady()) { - throw new Error("liquidity provider is not ready yet, disabled or unreachable") - } - const state = await this.GetUserState() - if (state.status === 'ERROR') { - throw new Error(state.reason) - } - return this.CalculateExpectedFeeLimit(amount, state) - } - - CanProviderHandle = async (req: LiquidityRequest): Promise => { + CanProviderHandle = async (req: LiquidityRequest): Promise => { if (!this.IsReady()) { this.log("provider is not ready") return false } - const state = await this.GetUserState() - if (state.status === 'ERROR') { - this.log("error getting user state", state.reason) - return false - } - const maxW = state.max_withdrawable + const maxW = this.GetMaxWithdrawable() if (req.action === 'spend' && maxW < req.amount) { return false } - return state + return true } AddInvoice = async (amount: number, memo: string, from: 'user' | 'system', expiry: number) => { @@ -257,38 +215,22 @@ export class LiquidityProvider { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") } - /* const userInfo = await this.GetUserState() - if (userInfo.status === 'ERROR') { - throw new Error(userInfo.reason) - } */ - const feeLimitToUse = feeLimit ? feeLimit : await this.GetExpectedFeeLimit(decodedAmount) - this.pendingPayments[invoice] = decodedAmount + feeLimitToUse //this.CalculateExpectedFeeLimit(decodedAmount, userInfo) - let acked = false + const feeLimitToUse = feeLimit ? feeLimit : this.CalculateExpectedFeeLimit(decodedAmount) + this.pendingPayments[invoice] = decodedAmount + feeLimitToUse const timeout = setTimeout(() => { - this.log("10 seconds passed, still waiting for ack") - this.GetUserState() + if (!this.pendingPaymentsAck[invoice]) { + return + } + this.log("10 seconds passed without a payment ack, locking provider until the next beacon") + this.lastSeenBeacon = 0 }, 1000 * 10) - const res = await new Promise((resolve, reject) => { - this.client.PayInvoiceStream({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }, (resp) => { - if (resp.status === 'ERROR') { - this.log("error paying invoice", resp.reason) - reject(new Error(resp.reason)) - return - } - if (resp.update.type === Types.InvoicePaymentStream_update_type.ACK) { - this.log("acked") - clearTimeout(timeout) - acked = true - return - } - resolve(resp.update.done) - }) - }) - //const res = await this.client.PayInvoice({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }) - /* if (res.status === 'ERROR') { + this.pendingPaymentsAck[invoice] = true + const res = await this.client.PayInvoice({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }) + delete this.pendingPaymentsAck[invoice] + if (res.status === 'ERROR') { this.log("error paying invoice", res.reason) throw new Error(res.reason) - } */ + } const totalPaid = res.amount_paid + res.network_fee + res.service_fee this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) this.latestReceivedBalance = res.latest_balance diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 4019167b..280c014f 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -251,27 +251,6 @@ export default class { const div = 1 + (totalBps / 10000) const max = Math.floor((balance - networkFeeFixed) / div) return { max, serviceFeeBps, networkFeeBps, networkFeeFixed } - - /* if (this.lnd.liquidProvider.IsReady()) { - const fees = this.lnd.liquidProvider.GetFees() - const providerServiceFee = fees.serviceFeeBps / 10000 - const providerNetworkFee = fees.networkFeeBps / 10000 - const div = 1 + serviceFee + providerServiceFee + providerNetworkFee - const max = Math.floor((balance - fees.networkFeeFixed) / div) - const networkFeeBps = fees.networkFeeBps + fees.serviceFeeBps - return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps, networkFeeFixed: fees.networkFeeFixed } - } - const { feeFixedLimit, feeRateLimit, feeRateBps } = this.settings.getSettings().lndSettings - const div = 1 + serviceFee + feeRateLimit - const max = Math.floor((balance - feeFixedLimit) / div) - return { max, serviceFeeBps: outgoingAppUserInvoiceFeeBps, networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit } */ - /* let maxWithinServiceFee = 0 - if (appUser) { - maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee))) - } else { - maxWithinServiceFee = Math.max(0, Math.floor(balance * (1 - this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee))) - } - return this.lnd.GetMaxWithinLimit(maxWithinServiceFee) */ } async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise { const decoded = await this.lnd.DecodeInvoice(req.invoice) @@ -280,17 +259,7 @@ export default class { } } - async PayInvoiceStream(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, cb: (res: Types.InvoicePaymentStream, err: Error | null) => void) { - const ack = () => cb({ update: { type: Types.InvoicePaymentStream_update_type.ACK, ack: {} } }, null) - try { - const paid = await this.PayInvoice(userId, req, linkedApplication, ack) - cb({ update: { type: Types.InvoicePaymentStream_update_type.DONE, done: paid } }, null) - } catch (err: any) { - cb({ update: { type: Types.InvoicePaymentStream_update_type.ACK, ack: {} } }, err) - } - } - - async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, ack?: () => void): Promise { + async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application, ack?: (op: Types.UserOperation) => void): Promise { await this.watchDog.PaymentRequested() const maybeBanned = await this.storage.userStorage.GetUser(userId) if (maybeBanned.locked) { @@ -328,13 +297,16 @@ export default class { } const user = await this.storage.userStorage.GetUser(userId) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId, appId: linkedApplication.app_id, appUserId: "", balance: user.balance_sats, data: req.invoice, amount: payAmount }) + const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}` + const operation = this.newInvoicePaymentOperation({ invoice: req.invoice, opId, amount: paymentInfo.amtPaid, networkFee: paymentInfo.networkFee, serviceFee: serviceFee, confirmed: true }) return { preimage: paymentInfo.preimage, amount_paid: paymentInfo.amtPaid, - operation_id: `${Types.UserOperationType.OUTGOING_INVOICE}-${paymentInfo.serialId}`, + operation_id: opId, network_fee: paymentInfo.networkFee, service_fee: serviceFee, - latest_balance: user.balance_sats + latest_balance: user.balance_sats, + operation } } @@ -353,7 +325,7 @@ export default class { return { use: 'provider', feeLimit: inputLimit || use.feeLimit } } - async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number, feeLimit?: number }, linkedApplication: Application, debitNpub?: string, ack?: () => void) { + async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number, feeLimit?: number }, linkedApplication: Application, debitNpub?: string, ack?: (op: Types.UserOperation) => void) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) { throw new Error("something went wrong sending payment, please try again later") } @@ -379,7 +351,9 @@ export default class { return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, debitNpub) }, "payment started") this.log("ready to pay") - ack?.() + const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${pendingPayment.serial_id}` + const op = this.newInvoicePaymentOperation({ invoice, opId, amount: payAmount, networkFee: routingFeeLimit, serviceFee: serviceFee, confirmed: false }) + ack?.(op) try { const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) @@ -414,10 +388,8 @@ export default class { } catch (err) { await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "internal_payment_refund:" + internalInvoice.invoice) this.utils.stateBundler.AddTxPointFailed('paidAnInvoice', payAmount, { used: 'internal', from: 'user' }, linkedApplication.app_id) - throw err } - } @@ -732,6 +704,23 @@ export default class { } } + newInvoicePaymentOperation = (opInfo: { invoice: string, opId: string, amount: number, networkFee: number, serviceFee: number, confirmed: boolean }): Types.UserOperation => { + const { invoice, opId, amount, networkFee, serviceFee, confirmed } = opInfo + return { + amount: amount, + paidAtUnix: Math.floor(Date.now() / 1000), + inbound: false, + type: Types.UserOperationType.OUTGOING_INVOICE, + identifier: invoice, + operationId: opId, + network_fee: networkFee, + service_fee: serviceFee, + confirmed, + tx_hash: "", + internal: networkFee === 0 + } + } + async GetPaymentState(userId: string, req: Types.GetPaymentStateRequest): Promise { const user = await this.storage.userStorage.GetUser(userId) if (user.locked) { diff --git a/src/services/serverMethods/index.ts b/src/services/serverMethods/index.ts index 16051c68..c9f92932 100644 --- a/src/services/serverMethods/index.ts +++ b/src/services/serverMethods/index.ts @@ -158,13 +158,6 @@ export default (mainHandler: Main): Types.ServerMethods => { if (err != null) throw new Error(err.message) return mainHandler.appUserManager.PayInvoice(ctx, req) }, - PayInvoiceStream: async ({ ctx, req, cb }) => { - const err = Types.PayInvoiceRequestValidate(req, { - invoice_CustomCheck: invoice => invoice !== '' - }) - if (err != null) throw new Error(err.message) - mainHandler.appUserManager.PayInvoiceStream(ctx, req, cb) - }, GetLnurlWithdrawLink: ({ ctx }) => mainHandler.paymentManager.GetLnurlWithdrawLink(ctx), GetLnurlWithdrawInfo: async ({ ctx, query }) => { if (!query.k1) { From 1418db224eabe0912cc337b9fe5b191f6a841c1d Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 24 Nov 2025 18:44:11 +0000 Subject: [PATCH 15/29] fix --- src/services/main/liquidityManager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index e03b1e02..bcec0f76 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -87,7 +87,7 @@ export class LiquidityManager { if (!providerReady) { throw new Error("cannot use liquidity provider, it is not ready") } - const feeLimit = await this.liquidityProvider.GetExpectedFeeLimit(amount) + const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount) return { use: 'provider', feeLimit } } if (!providerReady) { @@ -97,7 +97,7 @@ export class LiquidityManager { if (!canHandle) { return { use: 'lnd' } } - const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount, canHandle) + const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount) return { use: 'provider', feeLimit } } From 6985f51d9935fd3f7eb1c1e0a0b9ce6cf867f7d9 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Mon, 24 Nov 2025 19:00:43 +0000 Subject: [PATCH 16/29] cleanup --- src/services/lnd/lnd.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index eda107e2..6b9ada2b 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -346,10 +346,6 @@ export default class { return Math.ceil(amount * this.getSettings().lndSettings.feeRateLimit + this.getSettings().lndSettings.feeFixedLimit); } - /* GetMaxWithinLimit(amount: number): number { - return Math.max(0, Math.floor(amount * (1 - this.getSettings().lndSettings.feeRateLimit) - this.getSettings().lndSettings.feeFixedLimit)) - } */ - async ChannelBalance(): Promise<{ local: number, remote: number }> { // console.log("Getting channel balance") const res = await this.lightning.channelBalance({}) From 31751d83c32f2b38601f0c61e91657853e8b2314 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 16:26:30 +0000 Subject: [PATCH 17/29] service fee only --- proto/autogenerated/client.md | 3 - proto/autogenerated/go/types.go | 9 +- proto/autogenerated/ts/types.ts | 23 +--- proto/service/structs.proto | 7 +- src/services/lnd/lnd.ts | 10 +- src/services/main/appUserManager.ts | 5 +- src/services/main/applicationManager.ts | 10 +- src/services/main/index.ts | 6 +- src/services/main/liquidityManager.ts | 18 +-- src/services/main/liquidityProvider.ts | 52 +++++--- src/services/main/paymentManager.ts | 153 ++++++++++++++++-------- src/tests/setupBootstrapped.ts | 2 +- 12 files changed, 173 insertions(+), 125 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 42f5621b..2d623c0f 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1145,7 +1145,6 @@ The nostr server will send back a message response, and inside the body there wi - __invitation_link__: _string_ ### CumulativeFees - - __networkFeeBps__: _number_ - __networkFeeFixed__: _number_ - __serviceFeeBps__: _number_ @@ -1473,14 +1472,12 @@ The nostr server will send back a message response, and inside the body there wi ### PayAppUserInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional - - __fee_limit_sats__: _number_ *this field is optional - __invoice__: _string_ - __user_identifier__: _string_ ### PayInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional - - __fee_limit_sats__: _number_ *this field is optional - __invoice__: _string_ ### PayInvoiceResponse diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 5b9fb05d..4c281876 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -230,7 +230,6 @@ type CreateOneTimeInviteLinkResponse struct { Invitation_link string `json:"invitation_link"` } type CumulativeFees struct { - Networkfeebps int64 `json:"networkFeeBps"` Networkfeefixed int64 `json:"networkFeeFixed"` Servicefeebps int64 `json:"serviceFeeBps"` } @@ -558,15 +557,13 @@ type PayAddressResponse struct { type PayAppUserInvoiceRequest struct { Amount int64 `json:"amount"` Debit_npub string `json:"debit_npub"` - Fee_limit_sats int64 `json:"fee_limit_sats"` Invoice string `json:"invoice"` User_identifier string `json:"user_identifier"` } type PayInvoiceRequest struct { - Amount int64 `json:"amount"` - Debit_npub string `json:"debit_npub"` - Fee_limit_sats int64 `json:"fee_limit_sats"` - Invoice string `json:"invoice"` + Amount int64 `json:"amount"` + Debit_npub string `json:"debit_npub"` + Invoice string `json:"invoice"` } type PayInvoiceResponse struct { Amount_paid int64 `json:"amount_paid"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index f4d1764b..355973fe 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -1295,14 +1295,12 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL } export type CumulativeFees = { - networkFeeBps: number networkFeeFixed: number serviceFeeBps: number } export const CumulativeFeesOptionalFields: [] = [] export type CumulativeFeesOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] - networkFeeBps_CustomCheck?: (v: number) => boolean networkFeeFixed_CustomCheck?: (v: number) => boolean serviceFeeBps_CustomCheck?: (v: number) => boolean } @@ -1310,9 +1308,6 @@ export const CumulativeFeesValidate = (o?: CumulativeFees, opts: CumulativeFeesO if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') - if (typeof o.networkFeeBps !== 'number') return new Error(`${path}.networkFeeBps: is not a number`) - if (opts.networkFeeBps_CustomCheck && !opts.networkFeeBps_CustomCheck(o.networkFeeBps)) return new Error(`${path}.networkFeeBps: custom check failed`) - if (typeof o.networkFeeFixed !== 'number') return new Error(`${path}.networkFeeFixed: is not a number`) if (opts.networkFeeFixed_CustomCheck && !opts.networkFeeFixed_CustomCheck(o.networkFeeFixed)) return new Error(`${path}.networkFeeFixed: custom check failed`) @@ -3267,17 +3262,15 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr export type PayAppUserInvoiceRequest = { amount: number debit_npub?: string - fee_limit_sats?: number invoice: string user_identifier: string } -export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' | 'fee_limit_sats' -export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub', 'fee_limit_sats'] +export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' +export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub'] export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean - fee_limit_sats_CustomCheck?: (v?: number) => boolean invoice_CustomCheck?: (v: string) => boolean user_identifier_CustomCheck?: (v: string) => boolean } @@ -3291,9 +3284,6 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) - if ((o.fee_limit_sats || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('fee_limit_sats')) && typeof o.fee_limit_sats !== 'number') return new Error(`${path}.fee_limit_sats: is not a number`) - if (opts.fee_limit_sats_CustomCheck && !opts.fee_limit_sats_CustomCheck(o.fee_limit_sats)) return new Error(`${path}.fee_limit_sats: custom check failed`) - if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) @@ -3306,16 +3296,14 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o export type PayInvoiceRequest = { amount: number debit_npub?: string - fee_limit_sats?: number invoice: string } -export type PayInvoiceRequestOptionalField = 'debit_npub' | 'fee_limit_sats' -export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub', 'fee_limit_sats'] +export type PayInvoiceRequestOptionalField = 'debit_npub' +export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub'] export type PayInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean - fee_limit_sats_CustomCheck?: (v?: number) => boolean invoice_CustomCheck?: (v: string) => boolean } export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoiceRequestOptions = {}, path: string = 'PayInvoiceRequest::root.'): Error | null => { @@ -3328,9 +3316,6 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) - if ((o.fee_limit_sats || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('fee_limit_sats')) && typeof o.fee_limit_sats !== 'number') return new Error(`${path}.fee_limit_sats: is not a number`) - if (opts.fee_limit_sats_CustomCheck && !opts.fee_limit_sats_CustomCheck(o.fee_limit_sats)) return new Error(`${path}.fee_limit_sats: custom check failed`) - if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 78746db4..bb95fe3e 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -389,8 +389,7 @@ message PayAppUserInvoiceRequest { string user_identifier = 1; string invoice = 2; int64 amount = 3; - optional string debit_npub = 4; - optional int64 fee_limit_sats = 5; + optional string debit_npub = 4; } message SendAppUserToAppUserPaymentRequest { @@ -466,8 +465,7 @@ message DecodeInvoiceResponse{ message PayInvoiceRequest{ string invoice = 1; int64 amount = 2; - optional string debit_npub = 3; - optional int64 fee_limit_sats = 4; + optional string debit_npub = 3; } message PayInvoiceResponse{ @@ -831,7 +829,6 @@ message MessagingToken { message CumulativeFees { - int64 networkFeeBps = 1; int64 networkFeeFixed = 2; int64 serviceFeeBps = 3; } diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 6b9ada2b..aadf9e16 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -16,7 +16,7 @@ import { SendCoinsReq } from './sendCoinsReq.js'; import { AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js'; import { ERROR, getLogger } from '../helpers/logger.js'; import { HtlcEvent_EventType } from '../../../proto/lnd/router.js'; -import { LiquidityProvider, LiquidityRequest } from '../main/liquidityProvider.js'; +import { LiquidityProvider } from '../main/liquidityProvider.js'; import { Utils } from '../helpers/utilsWrapper.js'; import { TxPointSettings } from '../storage/tlv/stateBundler.js'; import { WalletKitClient } from '../../../proto/lnd/walletkit.client.js'; @@ -342,9 +342,9 @@ export default class { return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } } - GetFeeLimitAmount(amount: number): number { - return Math.ceil(amount * this.getSettings().lndSettings.feeRateLimit + this.getSettings().lndSettings.feeFixedLimit); - } + /* GetFeeLimitAmount(amount: number): number { + return Math.ceil(amount * this.getSettings().lndSettings.feeRateLimit + this.getSettings().lndSettings.feeFixedLimit); + } */ async ChannelBalance(): Promise<{ local: number, remote: number }> { // console.log("Getting channel balance") @@ -359,7 +359,7 @@ export default class { throw new Error("lnd node is currently out of sync") } if (useProvider) { - const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from, feeLimit) + const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from/* , feeLimit */) const providerDst = this.liquidProvider.GetProviderDestination() return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } } diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index f8d41b16..f79a171c 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -69,13 +69,13 @@ export default class { throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing } const nostrSettings = this.settings.getSettings().nostrRelaySettings - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) + const { max, /* networkFeeBps, */ networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) return { userId: ctx.user_id, balance: user.balance_sats, max_withdrawable: max, user_identifier: appUser.identifier, - network_max_fee_bps: networkFeeBps, + network_max_fee_bps: 0, network_max_fee_fixed: networkFeeFixed, service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: appUser.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), @@ -107,7 +107,6 @@ export default class { invoice: req.invoice, user_identifier: ctx.app_user_id, debit_npub: req.debit_npub, - fee_limit_sats: req.fee_limit_sats }) } diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index d2cc56fc..8dd92847 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -62,7 +62,7 @@ export default class { async StartAppsServiceBeacon(publishBeacon: (app: Application, fees: Types.CumulativeFees) => void) { this.serviceBeaconInterval = setInterval(async () => { try { - const fees = this.paymentManager.GetAllFees() + const fees = this.paymentManager.GetFees() const apps = await this.storage.applicationStorage.GetApplications() apps.forEach(app => { publishBeacon(app, fees) @@ -167,7 +167,7 @@ export default class { const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }) log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString }) - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats) + const { max, /* networkFeeBps, */networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats) return { identifier: u.identifier, info: { @@ -175,7 +175,7 @@ export default class { balance: u.user.balance_sats, max_withdrawable: max, user_identifier: u.identifier, - network_max_fee_bps: networkFeeBps, + network_max_fee_bps: 0, network_max_fee_fixed: networkFeeFixed, service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), @@ -225,13 +225,13 @@ export default class { const app = await this.storage.applicationStorage.GetApplication(appId) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) const nostrSettings = this.settings.getSettings().nostrRelaySettings - const { max, networkFeeBps, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) + const { max, /* networkFeeBps, */ networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) return { max_withdrawable: max, identifier: req.user_identifier, info: { userId: user.user.user_id, balance: user.user.balance_sats, max_withdrawable: max, user_identifier: user.identifier, - network_max_fee_bps: networkFeeBps, + network_max_fee_bps: 0, network_max_fee_fixed: networkFeeFixed, service_fee_bps: serviceFeeBps, noffer: nofferEncode({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: OfferPriceType.Spontaneous, relay: nostrSettings.relays[0] }), diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 2afe09ff..29e761a8 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -228,7 +228,7 @@ export default class { } log = getLogger({ appName: userAddress.linkedApplication.name }) const isAppUserPayment = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id - let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment) + let fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_TX, amount, isAppUserPayment) if (userAddress.linkedApplication && userAddress.linkedApplication.owner.user_id === userAddress.user.user_id) { fee = 0 } @@ -272,7 +272,7 @@ export default class { } log = getLogger({ appName: userInvoice.linkedApplication.name }) const isAppUserPayment = userInvoice.user.user_id !== userInvoice.linkedApplication.owner.user_id - let fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment) + let fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount, isAppUserPayment) if (userInvoice.linkedApplication && userInvoice.linkedApplication.owner.user_id === userInvoice.user.user_id) { fee = 0 } @@ -437,7 +437,7 @@ export default class { async ResetNostr() { const apps = await this.storage.applicationStorage.GetApplications() const nextRelay = this.settings.getSettings().nostrRelaySettings.relays[0] - const fees = this.paymentManager.GetAllFees() + const fees = this.paymentManager.GetFees() for (const app of apps) { await this.UpdateBeacon(app, { type: 'service', name: app.name, avatarUrl: app.avatar_url, nextRelay, fees }) } diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index bcec0f76..153ac94f 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -66,7 +66,7 @@ export class LiquidityManager { if (remote > amount) { return 'lnd' } - const providerCanHandle = await this.liquidityProvider.CanProviderHandle({ action: 'receive', amount }) + const providerCanHandle = this.liquidityProvider.IsReady() if (!providerCanHandle) { return 'lnd' } @@ -81,24 +81,24 @@ export class LiquidityManager { } } - beforeOutInvoicePayment = async (amount: number): Promise<{ use: 'lnd' } | { use: 'provider', feeLimit: number }> => { + beforeOutInvoicePayment = async (amount: number, localServiceFee: number): Promise<'lnd' | 'provider'> => { const providerReady = this.liquidityProvider.IsReady() if (this.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { if (!providerReady) { throw new Error("cannot use liquidity provider, it is not ready") } - const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount) - return { use: 'provider', feeLimit } + // const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount) + return 'provider' } if (!providerReady) { - return { use: 'lnd' } + return 'lnd' } - const canHandle = await this.liquidityProvider.CanProviderHandle({ action: 'spend', amount }) + const canHandle = await this.liquidityProvider.CanProviderPay(amount, localServiceFee) if (!canHandle) { - return { use: 'lnd' } + return 'lnd' } - const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount) - return { use: 'provider', feeLimit } + // const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount) + return 'provider' } afterOutInvoicePaid = async () => { } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 43f20bdc..6f66cf39 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -8,7 +8,6 @@ import { InvoicePaidCb } from '../lnd/settings.js' import Storage from '../storage/index.js' import SettingsManager from './settingsManager.js' import { LiquiditySettings } from './settings.js' -export type LiquidityRequest = { action: 'spend' | 'receive', amount: number } export type nostrCallback = { startedAtMillis: number, type: 'single' | 'stream', f: (res: T) => void } export class LiquidityProvider { getSettings: () => LiquiditySettings @@ -27,7 +26,7 @@ export class LiquidityProvider { queue: ((state: 'ready') => void)[] = [] utils: Utils pendingPayments: Record = {} - feesCache: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } | null = null + feesCache: Types.CumulativeFees | null = null lastSeenBeacon = 0 latestReceivedBalance = 0 incrementProviderBalance: (balance: number) => Promise @@ -132,7 +131,7 @@ export class LiquidityProvider { return res } this.feesCache = { - networkFeeBps: res.network_max_fee_bps, + // networkFeeBps: res.network_max_fee_bps, networkFeeFixed: res.network_max_fee_fixed, serviceFeeBps: res.service_fee_bps } @@ -153,10 +152,15 @@ export class LiquidityProvider { if (!this.IsReady() || !this.feesCache) { return 0 } - const { networkFeeBps, networkFeeFixed, serviceFeeBps } = this.feesCache - const totalBps = networkFeeBps + serviceFeeBps + const balance = this.latestReceivedBalance + const { /* networkFeeBps, */ networkFeeFixed, serviceFeeBps } = this.feesCache + const div = 1 + (serviceFeeBps / 10000) + const maxWithoutFixed = Math.floor(balance / div) + const fee = balance - maxWithoutFixed + return balance - Math.max(fee, networkFeeFixed) + /* const totalBps = networkFeeBps + serviceFeeBps const div = 1 + (totalBps / 10000) - return Math.floor((this.latestReceivedBalance - networkFeeFixed) / div) + return Math.floor((this.latestReceivedBalance - networkFeeFixed) / div) */ } GetLatestBalance = () => { @@ -170,24 +174,39 @@ export class LiquidityProvider { return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0) } - CalculateExpectedFeeLimit = (amount: number) => { + GetServiceFee = (amount: number) => { const fees = this.GetFees() const serviceFeeRate = fees.serviceFeeBps / 10000 const serviceFee = Math.ceil(serviceFeeRate * amount) - const networkMaxFeeRate = fees.networkFeeBps / 10000 - const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + fees.networkFeeFixed) - return serviceFee + networkFeeLimit + return Math.max(serviceFee, fees.networkFeeFixed) } - CanProviderHandle = async (req: LiquidityRequest): Promise => { + /* CalculateExpectedFeeLimit = (amount: number) => { + const fees = this.GetFees() + const serviceFeeRate = fees.serviceFeeBps / 10000 + const serviceFee = Math.ceil(serviceFeeRate * amount) + const networkMaxFeeRate = fees.networkFeeBps / 10000 + const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + fees.networkFeeFixed) + return serviceFee + networkFeeLimit + } */ + + CanProviderPay = async (amount: number, localServiceFee: number): Promise => { if (!this.IsReady()) { this.log("provider is not ready") return false } const maxW = this.GetMaxWithdrawable() - if (req.action === 'spend' && maxW < req.amount) { + if (maxW < amount) { + this.log("provider does not have enough funds to pay the invoice") return false } + + const providerServiceFee = this.GetServiceFee(amount) + if (localServiceFee < providerServiceFee) { + this.log(`local service fee ${localServiceFee} is less than the provider's service fee ${providerServiceFee}`) + return false + } + return true } @@ -210,13 +229,14 @@ export class LiquidityProvider { } - PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system', feeLimit?: number) => { + PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system'/* , feeLimit?: number */) => { try { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") } - const feeLimitToUse = feeLimit ? feeLimit : this.CalculateExpectedFeeLimit(decodedAmount) - this.pendingPayments[invoice] = decodedAmount + feeLimitToUse + // const feeLimitToUse = feeLimit ? feeLimit : this.CalculateExpectedFeeLimit(decodedAmount) + const providerServiceFee = this.GetServiceFee(decodedAmount) + this.pendingPayments[invoice] = decodedAmount + providerServiceFee const timeout = setTimeout(() => { if (!this.pendingPaymentsAck[invoice]) { return @@ -225,7 +245,7 @@ export class LiquidityProvider { this.lastSeenBeacon = 0 }, 1000 * 10) this.pendingPaymentsAck[invoice] = true - const res = await this.client.PayInvoice({ invoice, amount: 0, fee_limit_sats: feeLimitToUse }) + const res = await this.client.PayInvoice({ invoice, amount: 0,/* fee_limit_sats: feeLimitToUse */ }) delete this.pendingPaymentsAck[invoice] if (res.status === 'ERROR') { this.log("error paying invoice", res.reason) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 280c014f..87b88c22 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -162,23 +162,40 @@ export default class { } } - getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { + getReceiveServiceFee = (action: Types.UserOperationType, amount: number, appUser: boolean): number => { switch (action) { case Types.UserOperationType.INCOMING_TX: return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount) - case Types.UserOperationType.OUTGOING_TX: - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingTxFee * amount) case Types.UserOperationType.INCOMING_INVOICE: if (appUser) { return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount) } return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount) - case Types.UserOperationType.OUTGOING_INVOICE: + case Types.UserOperationType.INCOMING_USER_TO_USER: if (appUser) { - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount) + return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) } - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount) - case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER: + return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount) + default: + throw new Error("Unknown receive action type") + } + } + + getInvoicePaymentServiceFee = (amount: number, appUser: boolean): number => { + if (appUser) { + return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount) + } + return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount) + } + + getSendServiceFee = (action: Types.UserOperationType, amount: number, appUser: boolean): number => { + switch (action) { + case Types.UserOperationType.OUTGOING_TX: + throw new Error("Sending a transaction is not supported") + case Types.UserOperationType.OUTGOING_INVOICE: + const fee = this.getInvoicePaymentServiceFee(amount, appUser) + return Math.max(fee, this.settings.getSettings().lndSettings.feeFixedLimit) + case Types.UserOperationType.OUTGOING_USER_TO_USER: if (appUser) { return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) } @@ -188,6 +205,32 @@ export default class { } } + /* getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { + switch (action) { + case Types.UserOperationType.INCOMING_TX: + return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount) + case Types.UserOperationType.OUTGOING_TX: + return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingTxFee * amount) + case Types.UserOperationType.INCOMING_INVOICE: + if (appUser) { + return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount) + } + return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount) + case Types.UserOperationType.OUTGOING_INVOICE: + if (appUser) { + return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount) + } + return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount) + case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER: + if (appUser) { + return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) + } + return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount) + default: + throw new Error("Unknown service action type") + } + } */ + async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) { if (!this.settings.getSettings().lndSettings.mockLnd) { throw new Error("mock disabled, cannot set invoice as paid") @@ -234,23 +277,29 @@ export default class { } } - GetAllFees = (): Types.CumulativeFees => { + GetFees = (): Types.CumulativeFees => { const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings - if (this.lnd.liquidProvider.IsReady()) { + /* if (this.lnd.liquidProvider.IsReady()) { const fees = this.lnd.liquidProvider.GetFees() const networkFeeBps = fees.networkFeeBps + fees.serviceFeeBps return { networkFeeBps, networkFeeFixed: fees.networkFeeFixed, serviceFeeBps: outgoingAppUserInvoiceFeeBps } - } - const { feeFixedLimit, feeRateBps } = this.settings.getSettings().lndSettings - return { networkFeeBps: feeRateBps, networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps } + } */ + const { feeFixedLimit } = this.settings.getSettings().lndSettings + return { networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps } } GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } { - const { networkFeeBps, networkFeeFixed, serviceFeeBps } = this.GetAllFees() - const totalBps = networkFeeBps + serviceFeeBps - const div = 1 + (totalBps / 10000) - const max = Math.floor((balance - networkFeeFixed) / div) - return { max, serviceFeeBps, networkFeeBps, networkFeeFixed } + const { networkFeeFixed, serviceFeeBps } = this.GetFees() + const div = 1 + (serviceFeeBps / 10000) + const maxWithoutFixed = Math.floor(balance / div) + const fee = balance - maxWithoutFixed + const max = balance - Math.max(fee, networkFeeFixed) + return { max, networkFeeFixed, serviceFeeBps } + + /* const totalBps = networkFeeBps + serviceFeeBps + const div = 1 + (totalBps / 10000) + const max = Math.floor((balance - networkFeeFixed) / div) + return { max, serviceFeeBps, networkFeeBps, networkFeeFixed } */ } async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise { const decoded = await this.lnd.DecodeInvoice(req.invoice) @@ -274,10 +323,10 @@ export default class { } const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) const isAppUserPayment = userId !== linkedApplication.owner.user_id - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) - if (req.fee_limit_sats && req.fee_limit_sats < serviceFee) { - throw new Error("fee limit provided is too low to cover service fees") - } + const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) + /* if (req.fee_limit_sats && req.fee_limit_sats < serviceFee) { + throw new Error("fee limit provided is too low to cover service fees") + } */ const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice) if (internalInvoice && internalInvoice.paid_at_unix > 0) { throw new Error("this invoice was already paid") @@ -290,10 +339,11 @@ export default class { if (internalInvoice) { paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub) } else { - paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount, feeLimit: req.fee_limit_sats }, linkedApplication, req.debit_npub, ack) + paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount, /* feeLimit: req.fee_limit_sats */ }, linkedApplication, req.debit_npub, ack) } - if (isAppUserPayment && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee, "fees") + const feeDiff = serviceFee - paymentInfo.networkFee + if (isAppUserPayment && feeDiff > 0) { + await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, feeDiff, "fees") } const user = await this.storage.userStorage.GetUser(userId) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId, appId: linkedApplication.app_id, appUserId: "", balance: user.balance_sats, data: req.invoice, amount: payAmount }) @@ -310,22 +360,22 @@ export default class { } } - getUse = async (payAmount: number, inputLimit: number | undefined): Promise<{ use: 'lnd' | 'provider', feeLimit: number }> => { - const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) - if (use.use === 'lnd') { - const lndFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) - if (inputLimit && inputLimit < lndFeeLimit) { - this.log("WARNING requested fee limit is lower than suggested, payment might fail") + /* getUse = async (payAmount: number, localServiceFee: number): Promise<{ use: 'lnd' | 'provider', feeLimit: number }> => { + const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, localServiceFee) + if (use === 'lnd') { + const lndFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) + if (inputLimit && inputLimit < lndFeeLimit) { + this.log("WARNING requested fee limit is lower than suggested, payment might fail") + } + return { use: 'lnd', feeLimit: inputLimit || lndFeeLimit } } - return { use: 'lnd', feeLimit: inputLimit || lndFeeLimit } - } - if (inputLimit && inputLimit < use.feeLimit) { - this.log("WARNING requested fee limit is lower than suggested by provider, payment might fail") - } - return { use: 'provider', feeLimit: inputLimit || use.feeLimit } - } + if (inputLimit && inputLimit < use.feeLimit) { + this.log("WARNING requested fee limit is lower than suggested by provider, payment might fail") + } + return { use: 'provider', feeLimit: inputLimit || use.feeLimit } + } */ - async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number, feeLimit?: number }, linkedApplication: Application, debitNpub?: string, ack?: (op: Types.UserOperation) => void) { + async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number, /* feeLimit?: number */ }, linkedApplication: Application, debitNpub?: string, ack?: (op: Types.UserOperation) => void) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) { throw new Error("something went wrong sending payment, please try again later") } @@ -341,33 +391,36 @@ export default class { const { amountForLnd, payAmount, serviceFee } = amounts const totalAmountToDecrement = payAmount + serviceFee + const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee) /* const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) */ - const remainingLimit = amounts.feeLimit ? amounts.feeLimit - serviceFee : undefined - const { use, feeLimit: routingFeeLimit } = await this.getUse(payAmount, remainingLimit) + // const remainingLimit = amounts.feeLimit ? amounts.feeLimit - serviceFee : undefined + // const { use, feeLimit: routingFeeLimit } = await this.getUse(payAmount, remainingLimit) const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined const pendingPayment = await this.storage.StartTransaction(async tx => { - await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx) - return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx, debitNpub) + await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, invoice, tx) + return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: 0 }, linkedApplication, provider, tx, debitNpub) }, "payment started") this.log("ready to pay") const opId = `${Types.UserOperationType.OUTGOING_INVOICE}-${pendingPayment.serial_id}` - const op = this.newInvoicePaymentOperation({ invoice, opId, amount: payAmount, networkFee: routingFeeLimit, serviceFee: serviceFee, confirmed: false }) + const op = this.newInvoicePaymentOperation({ invoice, opId, amount: payAmount, networkFee: 0, serviceFee: serviceFee, confirmed: false }) ack?.(op) try { - const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { + const payment = await this.lnd.PayInvoice(invoice, amountForLnd, serviceFee, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) }) - if (routingFeeLimit - payment.feeSat > 0) { - this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats") + /* const feeDiff = serviceFee - payment.feeSat + if (feeDiff > 0) { + // this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats") + this.log("") await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice) - } + } */ await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerDst) return { preimage: payment.paymentPreimage, amtPaid: payment.valueSat, networkFee: payment.feeSat, serialId: pendingPayment.serial_id } } catch (err) { - await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, "payment_refund:" + invoice) + await this.storage.userStorage.IncrementUserBalance(userId, totalAmountToDecrement, "payment_refund:" + invoice) await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, 0, 0, false) throw err } @@ -403,7 +456,7 @@ export default class { } const { blockHeight } = await this.lnd.GetInfo() const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) - const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) + const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) const isAppUserPayment = ctx.user_id !== app.owner.user_id const internalAddress = await this.storage.paymentStorage.GetAddressOwner(req.address) let txId = "" @@ -772,7 +825,7 @@ export default class { throw new Error("not enough balance to send payment") } const isAppUserPayment = fromUser.user_id !== linkedApplication.owner.user_id - let fee = this.getServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) + let fee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) const toDecrement = amount + fee const paymentEntry = await this.storage.paymentStorage.AddPendingUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication, tx) await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, toDecrement, `${toUserId}:${paymentEntry.serial_id}`, tx) diff --git a/src/tests/setupBootstrapped.ts b/src/tests/setupBootstrapped.ts index 9ea29d91..3184d254 100644 --- a/src/tests/setupBootstrapped.ts +++ b/src/tests/setupBootstrapped.ts @@ -45,7 +45,7 @@ export const initBootstrappedInstance = async (T: TestBase) => { bootstrapped.liquidityProvider.setNostrInfo({ clientId: liquidityProviderInfo.clientId, myPub: liquidityProviderInfo.publicKey }) await new Promise(res => { const interval = setInterval(async () => { - const canHandle = await bootstrapped.liquidityProvider.CanProviderHandle({ action: 'receive', amount: 2000 }) + const canHandle = bootstrapped.liquidityProvider.IsReady() console.log("can handle", canHandle) if (canHandle) { clearInterval(interval) From b959f046520e1691959abb3c90dd294aed09fcc2 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 16:41:52 +0000 Subject: [PATCH 18/29] change defualt fixed fee --- src/services/main/settings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index 91717fcf..4a6ceeea 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -109,7 +109,7 @@ export const LoadLndSettingsFromEnv = (dbEnv: Record lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb), feeRateBps: feeRateBps, feeRateLimit: feeRateBps / 10000, - feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 100, addToDb), + feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb), mockLnd: false } } From 7e260af2d83f7bf78f424a1b29436e9259548591 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 16:50:14 +0000 Subject: [PATCH 19/29] deb --- src/services/main/paymentManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 87b88c22..d4490227 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -193,6 +193,7 @@ export default class { case Types.UserOperationType.OUTGOING_TX: throw new Error("Sending a transaction is not supported") case Types.UserOperationType.OUTGOING_INVOICE: + console.log("fee fixed limit", this.settings.getSettings().lndSettings.feeFixedLimit) const fee = this.getInvoicePaymentServiceFee(amount, appUser) return Math.max(fee, this.settings.getSettings().lndSettings.feeFixedLimit) case Types.UserOperationType.OUTGOING_USER_TO_USER: From d2d22f81a16a3cbfbf171d0cad6d398a8f4df169 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 16:53:23 +0000 Subject: [PATCH 20/29] fix --- src/services/main/paymentManager.ts | 1 - src/tests/.env.test | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index d4490227..87b88c22 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -193,7 +193,6 @@ export default class { case Types.UserOperationType.OUTGOING_TX: throw new Error("Sending a transaction is not supported") case Types.UserOperationType.OUTGOING_INVOICE: - console.log("fee fixed limit", this.settings.getSettings().lndSettings.feeFixedLimit) const fee = this.getInvoicePaymentServiceFee(amount, appUser) return Math.max(fee, this.settings.getSettings().lndSettings.feeFixedLimit) case Types.UserOperationType.OUTGOING_USER_TO_USER: diff --git a/src/tests/.env.test b/src/tests/.env.test index 8d0e92e2..dd0221bc 100644 --- a/src/tests/.env.test +++ b/src/tests/.env.test @@ -5,7 +5,7 @@ DATABASE_FILE=db.sqlite JWT_SECRET=bigsecrethere ALLOW_BALANCE_MIGRATION=true OUTBOUND_MAX_FEE_BPS=60 -OUTBOUND_MAX_FEE_EXTRA_SATS=100 +OUTBOUND_MAX_FEE_EXTRA_SATS=10 INCOMING_CHAIN_FEE_ROOT_BPS=0 OUTGOING_CHAIN_FEE_ROOT_BPS=60 #this is applied only to withdrawls from application wallets INCOMING_INVOICE_FEE_ROOT_BPS=0 From 8e3198f3e7dc1beecdb36535302c53eba5a39238 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 17:10:29 +0000 Subject: [PATCH 21/29] fix tests --- src/tests/externalPayment.spec.ts | 8 ++++---- src/tests/internalPayment.spec.ts | 8 ++++---- src/tests/liquidityProvider.spec.ts | 18 +++++++++--------- src/tests/spamExternalPayments.spec.ts | 7 +++---- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/tests/externalPayment.spec.ts b/src/tests/externalPayment.spec.ts index a92d0054..70f61117 100644 --- a/src/tests/externalPayment.spec.ts +++ b/src/tests/externalPayment.spec.ts @@ -24,10 +24,10 @@ const testSuccessfulExternalPayment = async (T: TestBase) => { T.d("paid 500 sats invoice from user1") const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(u1.balance_sats).to.be.equal(1496) - T.d("user1 balance is now 1496 (2000 - (500 + 3 fee + 1 routing))") - expect(owner.balance_sats).to.be.equal(3) - T.d("app balance is 3 sats") + expect(u1.balance_sats).to.be.equal(1490) + T.d("user1 balance is now 1490 (2000 - (500 + 10fee))") + expect(owner.balance_sats).to.be.equal(9) + T.d("app balance is 9 sats") } diff --git a/src/tests/internalPayment.spec.ts b/src/tests/internalPayment.spec.ts index f94bb994..80b35675 100644 --- a/src/tests/internalPayment.spec.ts +++ b/src/tests/internalPayment.spec.ts @@ -23,10 +23,10 @@ const testSuccessfulInternalPayment = async (T: TestBase) => { const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) expect(u2.balance_sats).to.be.equal(1000) T.d("user2 balance is 1000") - expect(u1.balance_sats).to.be.equal(994) - T.d("user1 balance is 994 cuz he paid 6 sats fee") - expect(owner.balance_sats).to.be.equal(6) - T.d("app balance is 6 sats") + expect(u1.balance_sats).to.be.equal(990) + T.d("user1 balance is 990 cuz he paid 10 sats fee") + expect(owner.balance_sats).to.be.equal(10) + T.d("app balance is 10 sats") } const testFailedInternalPayment = async (T: TestBase) => { diff --git a/src/tests/liquidityProvider.spec.ts b/src/tests/liquidityProvider.spec.ts index 30b9f37d..fa2bb767 100644 --- a/src/tests/liquidityProvider.spec.ts +++ b/src/tests/liquidityProvider.spec.ts @@ -21,30 +21,30 @@ export default async (T: TestBase) => { const testInboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bUser: TestUserData) => { T.d("starting testInboundPaymentFromProvider") - const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 2000, memo: "liquidityTest" }) + const invoiceRes = await bootstrapped.appUserManager.NewInvoice({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }, { amountSats: 3000, memo: "liquidityTest" }) - await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100, 2000, { from: 'system', useProvider: false }) + await T.externalAccessToOtherLnd.PayInvoice(invoiceRes.invoice, 0, 100, 3000, { from: 'system', useProvider: false }) await new Promise((resolve) => setTimeout(resolve, 200)) const userBalance = await bootstrapped.appUserManager.GetUserInfo({ app_id: bUser.appId, user_id: bUser.userId, app_user_id: bUser.appUserIdentifier }) - T.expect(userBalance.balance).to.equal(2000) - T.d("user balance is 2000") + T.expect(userBalance.balance).to.equal(3000) + T.d("user balance is 3000") const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() - T.expect(providerBalance).to.equal(2000) - T.d("provider balance is 2000") + T.expect(providerBalance).to.equal(3000) + T.d("provider balance is 3000") T.d("testInboundPaymentFromProvider done") } const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, bootstrappedUser: TestUserData) => { T.d("starting testOutboundPaymentFromProvider") - const invoice = await T.externalAccessToOtherLnd.NewInvoice(1000, "", 60 * 60, { from: 'system', useProvider: false }) + const invoice = await T.externalAccessToOtherLnd.NewInvoice(2000, "", 60 * 60, { from: 'system', useProvider: false }) const ctx = { app_id: bootstrappedUser.appId, user_id: bootstrappedUser.userId, app_user_id: bootstrappedUser.appUserIdentifier } const res = await bootstrapped.appUserManager.PayInvoice(ctx, { invoice: invoice.payRequest, amount: 0 }) const userBalance = await bootstrapped.appUserManager.GetUserInfo(ctx) - T.expect(userBalance.balance).to.equal(986) // 2000 - (1000 + 6(x2) + 2) + T.expect(userBalance.balance).to.equal(988) // 3000 - (2000 + 12) const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() - T.expect(providerBalance).to.equal(992) // 2000 - (1000 + 6 +2) + T.expect(providerBalance).to.equal(998) // 3000 - (2000 + 12) T.d("testOutboundPaymentFromProvider done") } \ No newline at end of file diff --git a/src/tests/spamExternalPayments.spec.ts b/src/tests/spamExternalPayments.spec.ts index e7956632..df4d018f 100644 --- a/src/tests/spamExternalPayments.spec.ts +++ b/src/tests/spamExternalPayments.spec.ts @@ -29,16 +29,15 @@ const testSpamExternalPayment = async (T: TestBase) => { const failedPayments = res.filter(r => !r.success) console.log(failedPayments) failedPayments.forEach(f => expect(f.err).to.be.equal("not enough balance to decrement")) - successfulPayments.forEach(s => expect(s.result).to.contain({ amount_paid: 500, network_fee: 1, service_fee: 3 })) + successfulPayments.forEach(s => expect(s.result).to.contain({ amount_paid: 500, network_fee: 1, service_fee: 10 })) expect(successfulPayments.length).to.be.equal(3) expect(failedPayments.length).to.be.equal(7) T.d("3 payments succeeded, 7 failed as expected") const u = await T.main.storage.userStorage.GetUser(T.user1.userId) const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(u.balance_sats).to.be.equal(488) - T.d("user1 balance is now 488 (2000 - (500 + 3 fee + 1 routing) * 3)") + expect(u.balance_sats).to.be.equal(470) + T.d("user1 balance is now 470 (2000 - (500 + 10 fee) * 3)") expect(owner.balance_sats).to.be.equal(9) T.d("app balance is 9 sats") - } From fe098c9e3e9274259821647c06f3d77d312e398f Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 17:22:13 +0000 Subject: [PATCH 22/29] more fix --- src/tests/externalPayment.spec.ts | 9 ++++----- src/tests/liquidityProvider.spec.ts | 2 +- src/tests/spamExternalPayments.spec.ts | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/tests/externalPayment.spec.ts b/src/tests/externalPayment.spec.ts index 70f61117..7a03e828 100644 --- a/src/tests/externalPayment.spec.ts +++ b/src/tests/externalPayment.spec.ts @@ -28,7 +28,6 @@ const testSuccessfulExternalPayment = async (T: TestBase) => { T.d("user1 balance is now 1490 (2000 - (500 + 10fee))") expect(owner.balance_sats).to.be.equal(9) T.d("app balance is 9 sats") - } const testFailedExternalPayment = async (T: TestBase) => { @@ -41,11 +40,11 @@ const testFailedExternalPayment = async (T: TestBase) => { await expectThrowsAsync(T.main.paymentManager.PayInvoice(T.user1.userId, { invoice: invoice.payRequest, amount: 0 }, application), "not enough balance to decrement") T.d("payment failed as expected, with the expected error message") const u1 = await T.main.storage.userStorage.GetUser(T.user1.userId) - expect(u1.balance_sats).to.be.equal(1496) - T.d("user1 balance is still 1496") + expect(u1.balance_sats).to.be.equal(1490) + T.d("user1 balance is still 1490") const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) - expect(owner.balance_sats).to.be.equal(3) - T.d("app balance is still 3 sats") + expect(owner.balance_sats).to.be.equal(9) + T.d("app balance is still 9 sats") } const testSuccesfulReceivedExternalChainPayment = async (T: TestBase) => { diff --git a/src/tests/liquidityProvider.spec.ts b/src/tests/liquidityProvider.spec.ts index fa2bb767..ad3685b7 100644 --- a/src/tests/liquidityProvider.spec.ts +++ b/src/tests/liquidityProvider.spec.ts @@ -45,6 +45,6 @@ const testOutboundPaymentFromProvider = async (T: TestBase, bootstrapped: Main, T.expect(userBalance.balance).to.equal(988) // 3000 - (2000 + 12) const providerBalance = await bootstrapped.liquidityProvider.GetLatestBalance() - T.expect(providerBalance).to.equal(998) // 3000 - (2000 + 12) + T.expect(providerBalance).to.equal(988) // 3000 - (2000 + 12) T.d("testOutboundPaymentFromProvider done") } \ No newline at end of file diff --git a/src/tests/spamExternalPayments.spec.ts b/src/tests/spamExternalPayments.spec.ts index df4d018f..d566d527 100644 --- a/src/tests/spamExternalPayments.spec.ts +++ b/src/tests/spamExternalPayments.spec.ts @@ -37,7 +37,7 @@ const testSpamExternalPayment = async (T: TestBase) => { const owner = await T.main.storage.userStorage.GetUser(application.owner.user_id) expect(u.balance_sats).to.be.equal(470) T.d("user1 balance is now 470 (2000 - (500 + 10 fee) * 3)") - expect(owner.balance_sats).to.be.equal(9) - T.d("app balance is 9 sats") + expect(owner.balance_sats).to.be.equal(27) + T.d("app balance is 27 sats") } From cbcac825410007d560013ac915bea5e78e5d2dcc Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 17:57:17 +0000 Subject: [PATCH 23/29] cleanup --- src/services/lnd/lnd.ts | 6 +-- src/services/main/appUserManager.ts | 2 +- src/services/main/applicationManager.ts | 4 +- src/services/main/liquidityManager.ts | 2 - src/services/main/liquidityProvider.ts | 19 +------ src/services/main/paymentManager.ts | 69 +------------------------ src/tests/setupBootstrapped.ts | 1 - 7 files changed, 8 insertions(+), 95 deletions(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index aadf9e16..4644e491 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -342,10 +342,6 @@ export default class { return { numSatoshis: Number(res.response.numSatoshis), paymentHash: res.response.paymentHash } } - /* GetFeeLimitAmount(amount: number): number { - return Math.ceil(amount * this.getSettings().lndSettings.feeRateLimit + this.getSettings().lndSettings.feeFixedLimit); - } */ - async ChannelBalance(): Promise<{ local: number, remote: number }> { // console.log("Getting channel balance") const res = await this.lightning.channelBalance({}) @@ -359,7 +355,7 @@ export default class { throw new Error("lnd node is currently out of sync") } if (useProvider) { - const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from/* , feeLimit */) + const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from) const providerDst = this.liquidProvider.GetProviderDestination() return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } } diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index f79a171c..75498b24 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -69,7 +69,7 @@ export default class { throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing } const nostrSettings = this.settings.getSettings().nostrRelaySettings - const { max, /* networkFeeBps, */ networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) + const { max, networkFeeFixed, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) return { userId: ctx.user_id, balance: user.balance_sats, diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index 8dd92847..b1befeb8 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -167,7 +167,7 @@ export default class { const ndebitString = ndebitEncode({ pubkey: app.nostr_public_key!, pointer: u.identifier, relay: nostrSettings.relays[0] }) log("🔗 [DEBUG] Generated ndebit for user", { userId: u.user.user_id, ndebit: ndebitString }) - const { max, /* networkFeeBps, */networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats) + const { max, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats) return { identifier: u.identifier, info: { @@ -225,7 +225,7 @@ export default class { const app = await this.storage.applicationStorage.GetApplication(appId) const user = await this.storage.applicationStorage.GetApplicationUser(app, req.user_identifier) const nostrSettings = this.settings.getSettings().nostrRelaySettings - const { max, /* networkFeeBps, */ networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) + const { max, networkFeeFixed, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) return { max_withdrawable: max, identifier: req.user_identifier, info: { userId: user.user.user_id, balance: user.user.balance_sats, diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index 153ac94f..f90631ab 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -87,7 +87,6 @@ export class LiquidityManager { if (!providerReady) { throw new Error("cannot use liquidity provider, it is not ready") } - // const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount) return 'provider' } if (!providerReady) { @@ -97,7 +96,6 @@ export class LiquidityManager { if (!canHandle) { return 'lnd' } - // const feeLimit = this.liquidityProvider.CalculateExpectedFeeLimit(amount) return 'provider' } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 6f66cf39..7789a5e3 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -131,7 +131,6 @@ export class LiquidityProvider { return res } this.feesCache = { - // networkFeeBps: res.network_max_fee_bps, networkFeeFixed: res.network_max_fee_fixed, serviceFeeBps: res.service_fee_bps } @@ -158,9 +157,6 @@ export class LiquidityProvider { const maxWithoutFixed = Math.floor(balance / div) const fee = balance - maxWithoutFixed return balance - Math.max(fee, networkFeeFixed) - /* const totalBps = networkFeeBps + serviceFeeBps - const div = 1 + (totalBps / 10000) - return Math.floor((this.latestReceivedBalance - networkFeeFixed) / div) */ } GetLatestBalance = () => { @@ -181,15 +177,6 @@ export class LiquidityProvider { return Math.max(serviceFee, fees.networkFeeFixed) } - /* CalculateExpectedFeeLimit = (amount: number) => { - const fees = this.GetFees() - const serviceFeeRate = fees.serviceFeeBps / 10000 - const serviceFee = Math.ceil(serviceFeeRate * amount) - const networkMaxFeeRate = fees.networkFeeBps / 10000 - const networkFeeLimit = Math.ceil(amount * networkMaxFeeRate + fees.networkFeeFixed) - return serviceFee + networkFeeLimit - } */ - CanProviderPay = async (amount: number, localServiceFee: number): Promise => { if (!this.IsReady()) { this.log("provider is not ready") @@ -229,12 +216,11 @@ export class LiquidityProvider { } - PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system'/* , feeLimit?: number */) => { + PayInvoice = async (invoice: string, decodedAmount: number, from: 'user' | 'system') => { try { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") } - // const feeLimitToUse = feeLimit ? feeLimit : this.CalculateExpectedFeeLimit(decodedAmount) const providerServiceFee = this.GetServiceFee(decodedAmount) this.pendingPayments[invoice] = decodedAmount + providerServiceFee const timeout = setTimeout(() => { @@ -245,7 +231,7 @@ export class LiquidityProvider { this.lastSeenBeacon = 0 }, 1000 * 10) this.pendingPaymentsAck[invoice] = true - const res = await this.client.PayInvoice({ invoice, amount: 0,/* fee_limit_sats: feeLimitToUse */ }) + const res = await this.client.PayInvoice({ invoice, amount: 0 }) delete this.pendingPaymentsAck[invoice] if (res.status === 'ERROR') { this.log("error paying invoice", res.reason) @@ -312,7 +298,6 @@ export class LiquidityProvider { this.log("configured to send to ") } } - // fees: { networkFeeBps: number, networkFeeFixed: number, serviceFeeBps: number } onBeaconEvent = async (beaconData: { content: string, pub: string }) => { if (beaconData.pub !== this.pubDestination) { this.log(ERROR, "got beacon from invalid pub", beaconData.pub, this.pubDestination) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 87b88c22..b3fd4ba4 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -205,32 +205,6 @@ export default class { } } - /* getServiceFee(action: Types.UserOperationType, amount: number, appUser: boolean): number { - switch (action) { - case Types.UserOperationType.INCOMING_TX: - return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount) - case Types.UserOperationType.OUTGOING_TX: - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingTxFee * amount) - case Types.UserOperationType.INCOMING_INVOICE: - if (appUser) { - return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount) - } - return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount) - case Types.UserOperationType.OUTGOING_INVOICE: - if (appUser) { - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount) - } - return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount) - case Types.UserOperationType.OUTGOING_USER_TO_USER || Types.UserOperationType.INCOMING_USER_TO_USER: - if (appUser) { - return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) - } - return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount) - default: - throw new Error("Unknown service action type") - } - } */ - async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) { if (!this.settings.getSettings().lndSettings.mockLnd) { throw new Error("mock disabled, cannot set invoice as paid") @@ -279,11 +253,6 @@ export default class { GetFees = (): Types.CumulativeFees => { const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings - /* if (this.lnd.liquidProvider.IsReady()) { - const fees = this.lnd.liquidProvider.GetFees() - const networkFeeBps = fees.networkFeeBps + fees.serviceFeeBps - return { networkFeeBps, networkFeeFixed: fees.networkFeeFixed, serviceFeeBps: outgoingAppUserInvoiceFeeBps } - } */ const { feeFixedLimit } = this.settings.getSettings().lndSettings return { networkFeeFixed: feeFixedLimit, serviceFeeBps: outgoingAppUserInvoiceFeeBps } } @@ -295,11 +264,6 @@ export default class { const fee = balance - maxWithoutFixed const max = balance - Math.max(fee, networkFeeFixed) return { max, networkFeeFixed, serviceFeeBps } - - /* const totalBps = networkFeeBps + serviceFeeBps - const div = 1 + (totalBps / 10000) - const max = Math.floor((balance - networkFeeFixed) / div) - return { max, serviceFeeBps, networkFeeBps, networkFeeFixed } */ } async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise { const decoded = await this.lnd.DecodeInvoice(req.invoice) @@ -324,9 +288,6 @@ export default class { const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) const isAppUserPayment = userId !== linkedApplication.owner.user_id const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) - /* if (req.fee_limit_sats && req.fee_limit_sats < serviceFee) { - throw new Error("fee limit provided is too low to cover service fees") - } */ const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice) if (internalInvoice && internalInvoice.paid_at_unix > 0) { throw new Error("this invoice was already paid") @@ -339,7 +300,7 @@ export default class { if (internalInvoice) { paymentInfo = await this.PayInternalInvoice(userId, internalInvoice, { payAmount, serviceFee }, linkedApplication, req.debit_npub) } else { - paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount, /* feeLimit: req.fee_limit_sats */ }, linkedApplication, req.debit_npub, ack) + paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, req.debit_npub, ack) } const feeDiff = serviceFee - paymentInfo.networkFee if (isAppUserPayment && feeDiff > 0) { @@ -360,22 +321,7 @@ export default class { } } - /* getUse = async (payAmount: number, localServiceFee: number): Promise<{ use: 'lnd' | 'provider', feeLimit: number }> => { - const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, localServiceFee) - if (use === 'lnd') { - const lndFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) - if (inputLimit && inputLimit < lndFeeLimit) { - this.log("WARNING requested fee limit is lower than suggested, payment might fail") - } - return { use: 'lnd', feeLimit: inputLimit || lndFeeLimit } - } - if (inputLimit && inputLimit < use.feeLimit) { - this.log("WARNING requested fee limit is lower than suggested by provider, payment might fail") - } - return { use: 'provider', feeLimit: inputLimit || use.feeLimit } - } */ - - async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number, /* feeLimit?: number */ }, linkedApplication: Application, debitNpub?: string, ack?: (op: Types.UserOperation) => void) { + async PayExternalInvoice(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, amountForLnd: number }, linkedApplication: Application, debitNpub?: string, ack?: (op: Types.UserOperation) => void) { if (this.settings.getSettings().serviceSettings.disableExternalPayments) { throw new Error("something went wrong sending payment, please try again later") } @@ -392,10 +338,6 @@ export default class { const { amountForLnd, payAmount, serviceFee } = amounts const totalAmountToDecrement = payAmount + serviceFee const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee) - /* const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) - const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount) */ - // const remainingLimit = amounts.feeLimit ? amounts.feeLimit - serviceFee : undefined - // const { use, feeLimit: routingFeeLimit } = await this.getUse(payAmount, remainingLimit) const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined const pendingPayment = await this.storage.StartTransaction(async tx => { await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement, invoice, tx) @@ -409,13 +351,6 @@ export default class { const payment = await this.lnd.PayInvoice(invoice, amountForLnd, serviceFee, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) }) - /* const feeDiff = serviceFee - payment.feeSat - if (feeDiff > 0) { - // this.log("refund routing fee", routingFeeLimit, payment.feeSat, "sats") - this.log("") - await this.storage.userStorage.IncrementUserBalance(userId, routingFeeLimit - payment.feeSat, "routing_fee_refund:" + invoice) - } */ - await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerDst) return { preimage: payment.paymentPreimage, amtPaid: payment.valueSat, networkFee: payment.feeSat, serialId: pendingPayment.serial_id } diff --git a/src/tests/setupBootstrapped.ts b/src/tests/setupBootstrapped.ts index 3184d254..32aa3144 100644 --- a/src/tests/setupBootstrapped.ts +++ b/src/tests/setupBootstrapped.ts @@ -46,7 +46,6 @@ export const initBootstrappedInstance = async (T: TestBase) => { await new Promise(res => { const interval = setInterval(async () => { const canHandle = bootstrapped.liquidityProvider.IsReady() - console.log("can handle", canHandle) if (canHandle) { clearInterval(interval) res() From ed3442eb03413bf63dcc5873b6cc68b7f977dbd1 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 18:23:03 +0000 Subject: [PATCH 24/29] fix payment restore --- src/services/lnd/lnd.ts | 2 +- src/services/main/paymentManager.ts | 48 ++++++++++++++--------------- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/src/services/lnd/lnd.ts b/src/services/lnd/lnd.ts index 4644e491..5f71285c 100644 --- a/src/services/lnd/lnd.ts +++ b/src/services/lnd/lnd.ts @@ -357,7 +357,7 @@ export default class { if (useProvider) { const res = await this.liquidProvider.PayInvoice(invoice, decodedAmount, from) const providerDst = this.liquidProvider.GetProviderDestination() - return { feeSat: res.network_fee + res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } + return { feeSat: res.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst } } await this.Health() try { diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index b3fd4ba4..fe1ea9e9 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -87,7 +87,7 @@ export default class { checkPendingProviderPayment = async (log: PubLogger, p: UserInvoicePayment) => { const state = await this.lnd.liquidProvider.GetPaymentState(p.invoice) if (state.paid_at_unix < 0) { - const fullAmount = p.paid_amount + p.service_fees + p.routing_fees + const fullAmount = p.paid_amount + p.service_fees log("found a failed provider payment, refunding", fullAmount, "sats to user", p.user.user_id) await this.storage.StartTransaction(async tx => { await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx) @@ -96,18 +96,16 @@ export default class { return } else if (state.paid_at_unix > 0) { log("provider payment succeeded", p.serial_id, "updating payment info") - const routingFeeLimit = p.routing_fees const serviceFee = p.service_fees - const actualFee = state.network_fee + state.service_fee - await this.storage.StartTransaction(async tx => { - if (routingFeeLimit - actualFee > 0) { - this.log("refund pending provider payment routing fee", routingFeeLimit, actualFee, "sats") - await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) - } - await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, actualFee, p.service_fees, true, undefined, tx) - }, "pending provider payment success after restart") - if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees") + const networkFee = state.service_fee + await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, serviceFee, true) + const remainingFee = serviceFee - networkFee + if (remainingFee < 0) { + this.log("WARNING: provider fee was higher than expected,", remainingFee, "were lost") + } + + if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && remainingFee > 0) { + await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees") } const user = await this.storage.userStorage.GetUser(p.user.user_id) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId: p.user.user_id, appId: p.linkedApplication?.app_id || "", appUserId: "", balance: user.balance_sats, data: p.invoice, amount: p.paid_amount }) @@ -123,7 +121,6 @@ export default class { log(ERROR, "lnd payment not found for pending payment hash ", decoded.paymentHash) return } - switch (payment.status) { case Payment_PaymentStatus.UNKNOWN: log("pending payment in unknown state", p.serial_id, "no action will be performed") @@ -133,24 +130,22 @@ export default class { return case Payment_PaymentStatus.SUCCEEDED: log("pending payment succeeded", p.serial_id, "updating payment info") - const routingFeeLimit = p.routing_fees const serviceFee = p.service_fees - const actualFee = Number(payment.feeSat) - await this.storage.StartTransaction(async tx => { - if (routingFeeLimit - actualFee > 0) { - this.log("refund pending payment routing fee", routingFeeLimit, actualFee, "sats") - await this.storage.userStorage.IncrementUserBalance(p.user.user_id, routingFeeLimit - actualFee, "routing_fee_refund:" + p.invoice, tx) - } - await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, actualFee, p.service_fees, true, undefined, tx) - }, "pending payment success after restart") - if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && serviceFee > 0) { - await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, serviceFee, "fees") + const networkFee = Number(payment.feeSat) + + await this.storage.paymentStorage.UpdateExternalPayment(p.serial_id, networkFee, p.service_fees, true, undefined) + const remainingFee = serviceFee - networkFee + if (remainingFee < 0) { // should not be possible beacuse of the fee limit + this.log("WARNING: lnd fee was higher than expected,", remainingFee, "were lost") + } + if (p.linkedApplication && p.user.user_id !== p.linkedApplication.owner.user_id && remainingFee > 0) { + await this.storage.userStorage.IncrementUserBalance(p.linkedApplication.owner.user_id, remainingFee, "fees") } const user = await this.storage.userStorage.GetUser(p.user.user_id) this.storage.eventsLog.LogEvent({ type: 'invoice_payment', userId: p.user.user_id, appId: p.linkedApplication?.app_id || "", appUserId: "", balance: user.balance_sats, data: p.invoice, amount: p.paid_amount }) return case Payment_PaymentStatus.FAILED: - const fullAmount = p.paid_amount + p.service_fees + p.routing_fees + const fullAmount = p.paid_amount + p.service_fees log("found a failed pending payment, refunding", fullAmount, "sats to user", p.user.user_id) await this.storage.StartTransaction(async tx => { await this.storage.userStorage.IncrementUserBalance(p.user.user_id, fullAmount, "payment_refund:" + p.invoice, tx) @@ -303,6 +298,9 @@ export default class { paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, req.debit_npub, ack) } const feeDiff = serviceFee - paymentInfo.networkFee + if (feeDiff < 0) { + this.log("WARNING: provider fee was higher than expected,", feeDiff, "were lost") + } if (isAppUserPayment && feeDiff > 0) { await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, feeDiff, "fees") } From df4d7464dab057c1c1b91b30f2e877fe3c2a2459 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 18:29:45 +0000 Subject: [PATCH 25/29] fix diff log --- src/services/main/paymentManager.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index fe1ea9e9..f4ee41c2 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -298,9 +298,6 @@ export default class { paymentInfo = await this.PayExternalInvoice(userId, req.invoice, { payAmount, serviceFee, amountForLnd: req.amount }, linkedApplication, req.debit_npub, ack) } const feeDiff = serviceFee - paymentInfo.networkFee - if (feeDiff < 0) { - this.log("WARNING: provider fee was higher than expected,", feeDiff, "were lost") - } if (isAppUserPayment && feeDiff > 0) { await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, feeDiff, "fees") } @@ -350,6 +347,10 @@ export default class { this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) }) await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerDst) + const feeDiff = serviceFee - payment.feeSat + if (feeDiff < 0) { // should not happen to lnd beacuse of the fee limit, culd happen to provider if the fee used to calculate the provider fee are out of date + this.log("WARNING: network fee was higher than expected,", feeDiff, "were lost by", use === 'provider' ? "provider" : "lnd") + } return { preimage: payment.paymentPreimage, amtPaid: payment.valueSat, networkFee: payment.feeSat, serialId: pendingPayment.serial_id } } catch (err) { From 9e3086df0d7ba431886d77f7406068bb335b1048 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Wed, 26 Nov 2025 18:57:55 +0000 Subject: [PATCH 26/29] expected fees --- proto/autogenerated/client.md | 2 ++ proto/autogenerated/go/types.go | 16 +++++++++------- proto/autogenerated/ts/types.ts | 24 ++++++++++++++++++++---- proto/service/structs.proto | 2 ++ src/services/main/appUserManager.ts | 1 + src/services/main/liquidityProvider.ts | 10 ++++++---- src/services/main/paymentManager.ts | 8 ++++++++ 7 files changed, 48 insertions(+), 15 deletions(-) diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 2d623c0f..3a0817ff 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1472,12 +1472,14 @@ The nostr server will send back a message response, and inside the body there wi ### PayAppUserInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional + - __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional - __invoice__: _string_ - __user_identifier__: _string_ ### PayInvoiceRequest - __amount__: _number_ - __debit_npub__: _string_ *this field is optional + - __expected_fees__: _[CumulativeFees](#CumulativeFees)_ *this field is optional - __invoice__: _string_ ### PayInvoiceResponse diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index 4c281876..f1d22e26 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -555,15 +555,17 @@ type PayAddressResponse struct { Txid string `json:"txId"` } type PayAppUserInvoiceRequest struct { - Amount int64 `json:"amount"` - Debit_npub string `json:"debit_npub"` - Invoice string `json:"invoice"` - User_identifier string `json:"user_identifier"` + Amount int64 `json:"amount"` + Debit_npub string `json:"debit_npub"` + Expected_fees *CumulativeFees `json:"expected_fees"` + Invoice string `json:"invoice"` + User_identifier string `json:"user_identifier"` } type PayInvoiceRequest struct { - Amount int64 `json:"amount"` - Debit_npub string `json:"debit_npub"` - Invoice string `json:"invoice"` + Amount int64 `json:"amount"` + Debit_npub string `json:"debit_npub"` + Expected_fees *CumulativeFees `json:"expected_fees"` + Invoice string `json:"invoice"` } type PayInvoiceResponse struct { Amount_paid int64 `json:"amount_paid"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 355973fe..e0da46ac 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -3262,15 +3262,17 @@ export const PayAddressResponseValidate = (o?: PayAddressResponse, opts: PayAddr export type PayAppUserInvoiceRequest = { amount: number debit_npub?: string + expected_fees?: CumulativeFees invoice: string user_identifier: string } -export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' -export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub'] +export type PayAppUserInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees' +export const PayAppUserInvoiceRequestOptionalFields: PayAppUserInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees'] export type PayAppUserInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayAppUserInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean + expected_fees_Options?: CumulativeFeesOptions invoice_CustomCheck?: (v: string) => boolean user_identifier_CustomCheck?: (v: string) => boolean } @@ -3284,6 +3286,12 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) + if (typeof o.expected_fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('expected_fees')) { + const expected_feesErr = CumulativeFeesValidate(o.expected_fees, opts.expected_fees_Options, `${path}.expected_fees`) + if (expected_feesErr !== null) return expected_feesErr + } + + if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) @@ -3296,14 +3304,16 @@ export const PayAppUserInvoiceRequestValidate = (o?: PayAppUserInvoiceRequest, o export type PayInvoiceRequest = { amount: number debit_npub?: string + expected_fees?: CumulativeFees invoice: string } -export type PayInvoiceRequestOptionalField = 'debit_npub' -export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub'] +export type PayInvoiceRequestOptionalField = 'debit_npub' | 'expected_fees' +export const PayInvoiceRequestOptionalFields: PayInvoiceRequestOptionalField[] = ['debit_npub', 'expected_fees'] export type PayInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: PayInvoiceRequestOptionalField[] amount_CustomCheck?: (v: number) => boolean debit_npub_CustomCheck?: (v?: string) => boolean + expected_fees_Options?: CumulativeFeesOptions invoice_CustomCheck?: (v: string) => boolean } export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoiceRequestOptions = {}, path: string = 'PayInvoiceRequest::root.'): Error | null => { @@ -3316,6 +3326,12 @@ export const PayInvoiceRequestValidate = (o?: PayInvoiceRequest, opts: PayInvoic if ((o.debit_npub || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('debit_npub')) && typeof o.debit_npub !== 'string') return new Error(`${path}.debit_npub: is not a string`) if (opts.debit_npub_CustomCheck && !opts.debit_npub_CustomCheck(o.debit_npub)) return new Error(`${path}.debit_npub: custom check failed`) + if (typeof o.expected_fees === 'object' || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('expected_fees')) { + const expected_feesErr = CumulativeFeesValidate(o.expected_fees, opts.expected_fees_Options, `${path}.expected_fees`) + if (expected_feesErr !== null) return expected_feesErr + } + + if (typeof o.invoice !== 'string') return new Error(`${path}.invoice: is not a string`) if (opts.invoice_CustomCheck && !opts.invoice_CustomCheck(o.invoice)) return new Error(`${path}.invoice: custom check failed`) diff --git a/proto/service/structs.proto b/proto/service/structs.proto index bb95fe3e..f22e2c5d 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -390,6 +390,7 @@ message PayAppUserInvoiceRequest { string invoice = 2; int64 amount = 3; optional string debit_npub = 4; + optional CumulativeFees expected_fees = 5; } message SendAppUserToAppUserPaymentRequest { @@ -466,6 +467,7 @@ message PayInvoiceRequest{ string invoice = 1; int64 amount = 2; optional string debit_npub = 3; + optional CumulativeFees expected_fees = 4; } message PayInvoiceResponse{ diff --git a/src/services/main/appUserManager.ts b/src/services/main/appUserManager.ts index 75498b24..bf75246c 100644 --- a/src/services/main/appUserManager.ts +++ b/src/services/main/appUserManager.ts @@ -107,6 +107,7 @@ export default class { invoice: req.invoice, user_identifier: ctx.app_user_id, debit_npub: req.debit_npub, + expected_fees: req.expected_fees, }) } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 7789a5e3..3774c143 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -170,8 +170,8 @@ export class LiquidityProvider { return Object.values(this.pendingPayments).reduce((a, b) => a + b, 0) } - GetServiceFee = (amount: number) => { - const fees = this.GetFees() + GetServiceFee = (amount: number, f?: Types.CumulativeFees) => { + const fees = f ? f : this.GetFees() const serviceFeeRate = fees.serviceFeeBps / 10000 const serviceFee = Math.ceil(serviceFeeRate * amount) return Math.max(serviceFee, fees.networkFeeFixed) @@ -221,7 +221,8 @@ export class LiquidityProvider { if (!this.IsReady()) { throw new Error("liquidity provider is not ready yet, disabled or unreachable") } - const providerServiceFee = this.GetServiceFee(decodedAmount) + const fees = this.GetFees() + const providerServiceFee = this.GetServiceFee(decodedAmount, fees) this.pendingPayments[invoice] = decodedAmount + providerServiceFee const timeout = setTimeout(() => { if (!this.pendingPaymentsAck[invoice]) { @@ -231,8 +232,9 @@ export class LiquidityProvider { this.lastSeenBeacon = 0 }, 1000 * 10) this.pendingPaymentsAck[invoice] = true - const res = await this.client.PayInvoice({ invoice, amount: 0 }) + const res = await this.client.PayInvoice({ invoice, amount: 0, expected_fees: fees }) delete this.pendingPaymentsAck[invoice] + clearTimeout(timeout) if (res.status === 'ERROR') { this.log("error paying invoice", res.reason) throw new Error(res.reason) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index f4ee41c2..5366b364 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -273,6 +273,14 @@ export default class { if (maybeBanned.locked) { throw new Error("user is banned, cannot send payment") } + if (req.expected_fees) { + const { networkFeeFixed, serviceFeeBps } = req.expected_fees + const serviceFixed = this.settings.getSettings().lndSettings.feeFixedLimit + const serviceBps = this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps + if (serviceFixed !== networkFeeFixed || serviceBps !== serviceFeeBps) { + throw new Error("fees do not match the expected fees") + } + } const decoded = await this.lnd.DecodeInvoice(req.invoice) if (decoded.numSatoshis !== 0 && req.amount !== 0) { throw new Error("invoice has value, do not provide amount the the request") From 05012c62989ebfb1dbebb69a02eee61ecee2251f Mon Sep 17 00:00:00 2001 From: boufni95 Date: Thu, 27 Nov 2025 15:36:59 +0000 Subject: [PATCH 27/29] fix for network fee change --- src/services/lnd/lsp.ts | 2 +- src/services/main/liquidityManager.ts | 2 +- src/services/main/liquidityProvider.ts | 2 +- src/services/main/paymentManager.ts | 4 ++-- src/services/metrics/index.ts | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/services/lnd/lsp.ts b/src/services/lnd/lsp.ts index e734c62c..bfce76fb 100644 --- a/src/services/lnd/lsp.ts +++ b/src/services/lnd/lsp.ts @@ -101,7 +101,7 @@ export class FlashsatsLSP extends LSP { return null } const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11_invoice, decoded.numSatoshis, 'system') - const fees = +order.payment.fee_total_sat + res.network_fee + res.service_fee + const fees = +order.payment.fee_total_sat + res.service_fee this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) return { orderId: order.order_id, invoice: order.payment.bolt11_invoice, totalSats: +order.payment.order_total_sat, fees } diff --git a/src/services/main/liquidityManager.ts b/src/services/main/liquidityManager.ts index f90631ab..8e4d635c 100644 --- a/src/services/main/liquidityManager.ts +++ b/src/services/main/liquidityManager.ts @@ -132,7 +132,7 @@ export class LiquidityManager { try { const invoice = await this.lnd.NewInvoice(amt, "liqudity provider drain", defaultInvoiceExpiry, { from: 'system', useProvider: false }) const res = await this.liquidityProvider.PayInvoice(invoice.payRequest, amt, 'system') - const fees = res.network_fee + res.service_fee + const fees = res.service_fee this.feesPaid += fees this.updateLatestDrain(true, amt) } catch (err: any) { diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 3774c143..9012c5f5 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -239,7 +239,7 @@ export class LiquidityProvider { this.log("error paying invoice", res.reason) throw new Error(res.reason) } - const totalPaid = res.amount_paid + res.network_fee + res.service_fee + const totalPaid = res.amount_paid + res.service_fee this.incrementProviderBalance(-totalPaid).then(() => { delete this.pendingPayments[invoice] }) this.latestReceivedBalance = res.latest_balance this.utils.stateBundler.AddTxPoint('paidAnInvoice', decodedAmount, { used: 'provider', from, timeDiscount: true }) diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 5366b364..2ae747ff 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -317,7 +317,7 @@ export default class { preimage: paymentInfo.preimage, amount_paid: paymentInfo.amtPaid, operation_id: opId, - network_fee: paymentInfo.networkFee, + network_fee: 0, service_fee: serviceFee, latest_balance: user.balance_sats, operation @@ -728,7 +728,7 @@ export default class { return { paid_at_unix: invoice.paid_at_unix, amount: invoice.paid_amount, - network_fee: invoice.routing_fees, + network_fee: 0, service_fee: invoice.service_fees, } } diff --git a/src/services/metrics/index.ts b/src/services/metrics/index.ts index 9774d4c1..1e1b30c7 100644 --- a/src/services/metrics/index.ts +++ b/src/services/metrics/index.ts @@ -241,12 +241,12 @@ export default class Handler { ops.outgoingInvoices.forEach(i => { if (req.include_operations) operations.push({ type: Types.UserOperationType.OUTGOING_INVOICE, amount: i.paid_amount, inbound: false, paidAtUnix: i.paid_at_unix, confirmed: true, service_fee: i.service_fees, network_fee: i.routing_fees, identifier: "", operationId: "", tx_hash: "", internal: i.internal }) totalSpent += i.paid_amount - feesInRange += i.service_fees + feesInRange += (i.service_fees - i.routing_fees) }) ops.outgoingTransactions.forEach(tx => { if (req.include_operations) operations.push({ type: Types.UserOperationType.OUTGOING_TX, amount: tx.paid_amount, inbound: false, paidAtUnix: tx.paid_at_unix, confirmed: tx.confs > 1, service_fee: tx.service_fees, network_fee: tx.chain_fees, identifier: "", operationId: "", tx_hash: tx.tx_hash, internal: tx.internal }) totalSpent += tx.paid_amount - feesInRange += tx.service_fees + feesInRange += (tx.service_fees - tx.chain_fees) }) ops.userToUser.forEach(op => { From 62ac0dbbc0af3d5882739e6d7805c86aa4a316e8 Mon Sep 17 00:00:00 2001 From: boufni95 Date: Thu, 27 Nov 2025 15:40:30 +0000 Subject: [PATCH 28/29] test fix --- src/tests/spamExternalPayments.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/spamExternalPayments.spec.ts b/src/tests/spamExternalPayments.spec.ts index d566d527..c81567c5 100644 --- a/src/tests/spamExternalPayments.spec.ts +++ b/src/tests/spamExternalPayments.spec.ts @@ -29,7 +29,7 @@ const testSpamExternalPayment = async (T: TestBase) => { const failedPayments = res.filter(r => !r.success) console.log(failedPayments) failedPayments.forEach(f => expect(f.err).to.be.equal("not enough balance to decrement")) - successfulPayments.forEach(s => expect(s.result).to.contain({ amount_paid: 500, network_fee: 1, service_fee: 10 })) + successfulPayments.forEach(s => expect(s.result).to.contain({ amount_paid: 500, network_fee: 0, service_fee: 10 })) expect(successfulPayments.length).to.be.equal(3) expect(failedPayments.length).to.be.equal(7) T.d("3 payments succeeded, 7 failed as expected") From a5553c517bd49dd0a86002ed2476fc34c773fc3f Mon Sep 17 00:00:00 2001 From: boufni95 Date: Thu, 27 Nov 2025 17:01:05 +0000 Subject: [PATCH 29/29] more fixes --- src/services/lnd/lsp.ts | 4 ++-- src/services/main/debitManager.ts | 2 +- src/services/main/liquidityProvider.ts | 2 +- src/services/main/settings.ts | 5 ----- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/services/lnd/lsp.ts b/src/services/lnd/lsp.ts index bfce76fb..92643c2a 100644 --- a/src/services/lnd/lsp.ts +++ b/src/services/lnd/lsp.ts @@ -177,7 +177,7 @@ export class OlympusLSP extends LSP { return null } const res = await this.liquidityProvider.PayInvoice(order.payment.bolt11.invoice, decoded.numSatoshis, 'system') - const fees = +order.payment.bolt11.fee_total_sat + res.network_fee + res.service_fee + const fees = +order.payment.bolt11.fee_total_sat + res.service_fee this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) return { orderId: order.order_id, invoice: order.payment.bolt11.invoice, totalSats: +order.payment.bolt11.order_total_sat, fees } } @@ -279,7 +279,7 @@ export class VoltageLSP extends LSP { return null } const res = await this.liquidityProvider.PayInvoice(proposalRes.jit_bolt11, decoded.numSatoshis, 'system') - const fees = feeSats + res.network_fee + res.service_fee + const fees = feeSats + res.service_fee this.log("paid", res.amount_paid, "to open channel, and a fee of", fees) return { orderId: fee.id, invoice: proposalRes.jit_bolt11, totalSats: decoded.numSatoshis, fees } } diff --git a/src/services/main/debitManager.ts b/src/services/main/debitManager.ts index 11270629..e1d85310 100644 --- a/src/services/main/debitManager.ts +++ b/src/services/main/debitManager.ts @@ -289,7 +289,7 @@ export class DebitManager { sendDebitPayment = async (appId: string, appUserId: string, requestorPub: string, bolt11: string) => { const payment = await this.applicationManager.PayAppUserInvoice(appId, { amount: 0, invoice: bolt11, user_identifier: appUserId, debit_npub: requestorPub }) - await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee + payment.network_fee) + await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee) return { payment } } diff --git a/src/services/main/liquidityProvider.ts b/src/services/main/liquidityProvider.ts index 9012c5f5..963610a6 100644 --- a/src/services/main/liquidityProvider.ts +++ b/src/services/main/liquidityProvider.ts @@ -152,7 +152,7 @@ export class LiquidityProvider { return 0 } const balance = this.latestReceivedBalance - const { /* networkFeeBps, */ networkFeeFixed, serviceFeeBps } = this.feesCache + const { networkFeeFixed, serviceFeeBps } = this.feesCache const div = 1 + (serviceFeeBps / 10000) const maxWithoutFixed = Math.floor(balance / div) const fee = balance - maxWithoutFixed diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index 4a6ceeea..8ae28723 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -78,9 +78,7 @@ export type LndNodeSettings = { } export type LndSettings = { lndLogDir: string - feeRateLimit: number feeFixedLimit: number - feeRateBps: number mockLnd: boolean } @@ -104,11 +102,8 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): LndSettings => { - const feeRateBps: number = chooseEnvInt('OUTBOUND_MAX_FEE_BPS', dbEnv, 60, addToDb) return { lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb), - feeRateBps: feeRateBps, - feeRateLimit: feeRateBps / 10000, feeFixedLimit: chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb), mockLnd: false }