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)