diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index 976d0f0f..e576aab9 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -175,10 +175,8 @@ export default class { case Types.UserOperationType.INCOMING_TX: return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * 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) + // Incoming invoice fees are always 0 (not configurable) + return 0 case Types.UserOperationType.INCOMING_USER_TO_USER: if (appUser) { return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) @@ -202,7 +200,7 @@ export default class { 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.outboundFeeFloor) + return Math.max(fee, this.settings.getSettings().lndSettings.serviceFeeFloor) case Types.UserOperationType.OUTGOING_USER_TO_USER: if (appUser) { return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) @@ -213,6 +211,12 @@ export default class { } } + getRoutingFeeLimit = (amount: number): number => { + const { routingFeeLimitBps, routingFeeFloor } = this.settings.getSettings().lndSettings + const limit = Math.floor(amount * routingFeeLimitBps / 10000) + return Math.max(limit, routingFeeFloor) + } + async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) { if (!this.settings.getSettings().lndSettings.mockLnd) { throw new Error("mock disabled, cannot set invoice as paid") @@ -261,8 +265,8 @@ export default class { GetFees = (): Types.CumulativeFees => { const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings - const { outboundFeeFloor } = this.settings.getSettings().lndSettings - return { outboundFeeFloor, serviceFeeBps: outgoingAppUserInvoiceFeeBps } + const { serviceFeeFloor } = this.settings.getSettings().lndSettings + return { outboundFeeFloor: serviceFeeFloor, serviceFeeBps: outgoingAppUserInvoiceFeeBps } } GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } { @@ -288,7 +292,7 @@ export default class { } if (req.expected_fees) { const { outboundFeeFloor, serviceFeeBps } = req.expected_fees - const serviceFixed = this.settings.getSettings().lndSettings.outboundFeeFloor + const serviceFixed = this.settings.getSettings().lndSettings.serviceFeeFloor const serviceBps = this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps if (serviceFixed !== outboundFeeFloor || serviceBps !== serviceFeeBps) { throw new Error("fees do not match the expected fees") @@ -364,6 +368,7 @@ export default class { const { amountForLnd, payAmount, serviceFee } = amounts const totalAmountToDecrement = payAmount + serviceFee + const routingFeeLimit = this.getRoutingFeeLimit(payAmount) const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee) const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined const pendingPayment = await this.storage.StartTransaction(async tx => { @@ -375,7 +380,7 @@ export default class { const op = this.newInvoicePaymentOperation({ invoice, opId, amount: payAmount, networkFee: 0, serviceFee: serviceFee, confirmed: false }) optionals.ack?.(op) try { - const payment = await this.lnd.PayInvoice(invoice, amountForLnd, serviceFee, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { + const payment = await this.lnd.PayInvoice(invoice, amountForLnd, routingFeeLimit, payAmount, { useProvider: use === 'provider', from: 'user' }, index => { this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index) }) await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerDst) diff --git a/src/services/main/settings.ts b/src/services/main/settings.ts index 0d4ea352..aac27156 100644 --- a/src/services/main/settings.ts +++ b/src/services/main/settings.ts @@ -5,8 +5,6 @@ import path from 'path' export type ServiceFeeSettings = { incomingTxFee: number outgoingTxFee: number - incomingAppInvoiceFee: number - incomingAppUserInvoiceFee: number outgoingAppInvoiceFee: number outgoingAppUserInvoiceFee: number outgoingAppUserInvoiceFeeBps: number @@ -15,15 +13,32 @@ export type ServiceFeeSettings = { } export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): ServiceFeeSettings => { - const outgoingAppUserInvoiceFeeBps = chooseEnvInt("OUTGOING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb) + // Support both old and new env var names for backward compatibility (new name takes precedence) + // Check if new name exists first (in process.env or dbEnv) + const newExists = process.env["SERVICE_FEE_BPS"] !== undefined || dbEnv["SERVICE_FEE_BPS"] !== undefined + let serviceFeeBps: number + if (newExists) { + // New name exists, use it + serviceFeeBps = chooseEnvInt("SERVICE_FEE_BPS", dbEnv, 60, addToDb) + } else { + // New name doesn't exist, check old name for backward compatibility + const oldExists = process.env["OUTGOING_INVOICE_FEE_USER_BPS"] !== undefined || dbEnv["OUTGOING_INVOICE_FEE_USER_BPS"] !== undefined + if (oldExists) { + // Old name exists, use it and migrate to new name in DB + const oldValue = chooseEnvInt("OUTGOING_INVOICE_FEE_USER_BPS", dbEnv, 60) // Don't add old name to DB + serviceFeeBps = oldValue + if (addToDb) addToDb("SERVICE_FEE_BPS", oldValue.toString()) // Migrate to new name + } else { + // Neither exists, use default with new name + serviceFeeBps = chooseEnvInt("SERVICE_FEE_BPS", dbEnv, 60, addToDb) + } + } return { - incomingTxFee: chooseEnvInt("INCOMING_CHAIN_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000, + incomingTxFee: 0, // Not configurable, always 0 outgoingTxFee: chooseEnvInt("OUTGOING_CHAIN_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000, - incomingAppInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_ROOT_BPS", dbEnv, 0, addToDb) / 10000, outgoingAppInvoiceFee: chooseEnvInt("OUTGOING_INVOICE_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000, - incomingAppUserInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb) / 10000, - outgoingAppUserInvoiceFeeBps, - outgoingAppUserInvoiceFee: outgoingAppUserInvoiceFeeBps / 10000, + outgoingAppUserInvoiceFeeBps: serviceFeeBps, + outgoingAppUserInvoiceFee: serviceFeeBps / 10000, userToUserFee: chooseEnvInt("TX_FEE_INTERNAL_USER_BPS", dbEnv, 0, addToDb) / 10000, appToUserFee: chooseEnvInt("TX_FEE_INTERNAL_ROOT_BPS", dbEnv, 0, addToDb) / 10000, } @@ -80,7 +95,9 @@ const networks = ['mainnet', 'testnet', 'regtest'] as const export type BTCNetwork = (typeof networks)[number] export type LndSettings = { lndLogDir: string - outboundFeeFloor: number + serviceFeeFloor: number + routingFeeLimitBps: number + routingFeeFloor: number mockLnd: boolean network: BTCNetwork } @@ -112,11 +129,44 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record, addToDb?: EnvCacher): LndSettings => { const network = chooseEnv('BTC_NETWORK', dbEnv, 'mainnet', addToDb) as BTCNetwork - const limitOld = chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb) - const outboundFeeFloor = chooseEnvInt('OUTBOUND_FEE_FLOOR_SATS', dbEnv, limitOld, addToDb) + + // Routing fee floor: new name takes precedence, fall back to old name for backward compatibility + const routingFeeFloorNewExists = process.env['ROUTING_FEE_FLOOR_SATS'] !== undefined || dbEnv['ROUTING_FEE_FLOOR_SATS'] !== undefined + let routingFeeFloor: number + if (routingFeeFloorNewExists) { + routingFeeFloor = chooseEnvInt('ROUTING_FEE_FLOOR_SATS', dbEnv, 5, addToDb) + } else { + const routingFeeFloorOldExists = process.env['OUTBOUND_MAX_FEE_EXTRA_SATS'] !== undefined || dbEnv['OUTBOUND_MAX_FEE_EXTRA_SATS'] !== undefined + if (routingFeeFloorOldExists) { + const oldValue = chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 5) // Don't add old name to DB + routingFeeFloor = oldValue + if (addToDb) addToDb('ROUTING_FEE_FLOOR_SATS', oldValue.toString()) // Migrate to new name + } else { + routingFeeFloor = chooseEnvInt('ROUTING_FEE_FLOOR_SATS', dbEnv, 5, addToDb) + } + } + + // Service fee floor: new name takes precedence, fall back to old name for backward compatibility + const serviceFeeFloorNewExists = process.env['SERVICE_FEE_FLOOR_SATS'] !== undefined || dbEnv['SERVICE_FEE_FLOOR_SATS'] !== undefined + let serviceFeeFloor: number + if (serviceFeeFloorNewExists) { + serviceFeeFloor = chooseEnvInt('SERVICE_FEE_FLOOR_SATS', dbEnv, 10, addToDb) + } else { + const serviceFeeFloorOldExists = process.env['OUTBOUND_MAX_FEE_EXTRA_SATS'] !== undefined || dbEnv['OUTBOUND_MAX_FEE_EXTRA_SATS'] !== undefined + if (serviceFeeFloorOldExists) { + const oldValue = chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10) // Don't add old name to DB + serviceFeeFloor = oldValue + if (addToDb) addToDb('SERVICE_FEE_FLOOR_SATS', oldValue.toString()) // Migrate to new name + } else { + serviceFeeFloor = chooseEnvInt('SERVICE_FEE_FLOOR_SATS', dbEnv, 10, addToDb) + } + } + const routingFeeLimitBps = chooseEnvInt('ROUTING_FEE_LIMIT_BPS', dbEnv, 50, addToDb) return { lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb), - outboundFeeFloor, + serviceFeeFloor, + routingFeeLimitBps, + routingFeeFloor, mockLnd: false, network: networks.includes(network) ? network : 'mainnet' } diff --git a/src/services/main/settingsManager.ts b/src/services/main/settingsManager.ts index e2e2d001..f56b4b6f 100644 --- a/src/services/main/settingsManager.ts +++ b/src/services/main/settingsManager.ts @@ -50,9 +50,26 @@ export default class SettingsManager { for (const key in toAdd) { await this.storage.settingsStorage.setDbEnvIFNeeded(key, toAdd[key]) } + // Validate fee configuration: routing fee limit must be <= service fee + this.validateFeeSettings(this.settings) return this.settings } + private validateFeeSettings(settings: FullSettings): void { + const { serviceFeeSettings, lndSettings } = settings + const serviceFeeBps = serviceFeeSettings.outgoingAppUserInvoiceFeeBps + const routingFeeLimitBps = lndSettings.routingFeeLimitBps + const serviceFeeFloor = lndSettings.serviceFeeFloor + const routingFeeFloor = lndSettings.routingFeeFloor + + if (routingFeeLimitBps > serviceFeeBps) { + throw new Error(`ROUTING_FEE_LIMIT_BPS (${routingFeeLimitBps}) must be <= SERVICE_FEE_BPS (${serviceFeeBps}) to ensure Pub keeps a spread`) + } + if (routingFeeFloor > serviceFeeFloor) { + throw new Error(`ROUTING_FEE_FLOOR_SATS (${routingFeeFloor}) must be <= SERVICE_FEE_FLOOR_SATS (${serviceFeeFloor}) to ensure Pub keeps a spread`) + } + } + getStorageSettings(): StorageSettings { return this.storage.getStorageSettings() }