cleanup fee design
This commit is contained in:
parent
5cb1cd509d
commit
ad8cd91aad
3 changed files with 93 additions and 21 deletions
|
|
@ -175,10 +175,8 @@ export default class {
|
||||||
case Types.UserOperationType.INCOMING_TX:
|
case Types.UserOperationType.INCOMING_TX:
|
||||||
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount)
|
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount)
|
||||||
case Types.UserOperationType.INCOMING_INVOICE:
|
case Types.UserOperationType.INCOMING_INVOICE:
|
||||||
if (appUser) {
|
// Incoming invoice fees are always 0 (not configurable)
|
||||||
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppUserInvoiceFee * amount)
|
return 0
|
||||||
}
|
|
||||||
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingAppInvoiceFee * amount)
|
|
||||||
case Types.UserOperationType.INCOMING_USER_TO_USER:
|
case Types.UserOperationType.INCOMING_USER_TO_USER:
|
||||||
if (appUser) {
|
if (appUser) {
|
||||||
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
|
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")
|
throw new Error("Sending a transaction is not supported")
|
||||||
case Types.UserOperationType.OUTGOING_INVOICE:
|
case Types.UserOperationType.OUTGOING_INVOICE:
|
||||||
const fee = this.getInvoicePaymentServiceFee(amount, appUser)
|
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:
|
case Types.UserOperationType.OUTGOING_USER_TO_USER:
|
||||||
if (appUser) {
|
if (appUser) {
|
||||||
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
|
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) {
|
async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) {
|
||||||
if (!this.settings.getSettings().lndSettings.mockLnd) {
|
if (!this.settings.getSettings().lndSettings.mockLnd) {
|
||||||
throw new Error("mock disabled, cannot set invoice as paid")
|
throw new Error("mock disabled, cannot set invoice as paid")
|
||||||
|
|
@ -261,8 +265,8 @@ export default class {
|
||||||
|
|
||||||
GetFees = (): Types.CumulativeFees => {
|
GetFees = (): Types.CumulativeFees => {
|
||||||
const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings
|
const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings
|
||||||
const { outboundFeeFloor } = this.settings.getSettings().lndSettings
|
const { serviceFeeFloor } = this.settings.getSettings().lndSettings
|
||||||
return { outboundFeeFloor, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
|
return { outboundFeeFloor: serviceFeeFloor, serviceFeeBps: outgoingAppUserInvoiceFeeBps }
|
||||||
}
|
}
|
||||||
|
|
||||||
GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
|
GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
|
||||||
|
|
@ -288,7 +292,7 @@ export default class {
|
||||||
}
|
}
|
||||||
if (req.expected_fees) {
|
if (req.expected_fees) {
|
||||||
const { outboundFeeFloor, serviceFeeBps } = 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
|
const serviceBps = this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps
|
||||||
if (serviceFixed !== outboundFeeFloor || serviceBps !== serviceFeeBps) {
|
if (serviceFixed !== outboundFeeFloor || serviceBps !== serviceFeeBps) {
|
||||||
throw new Error("fees do not match the expected fees")
|
throw new Error("fees do not match the expected fees")
|
||||||
|
|
@ -364,6 +368,7 @@ export default class {
|
||||||
|
|
||||||
const { amountForLnd, payAmount, serviceFee } = amounts
|
const { amountForLnd, payAmount, serviceFee } = amounts
|
||||||
const totalAmountToDecrement = payAmount + serviceFee
|
const totalAmountToDecrement = payAmount + serviceFee
|
||||||
|
const routingFeeLimit = this.getRoutingFeeLimit(payAmount)
|
||||||
const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee)
|
const use = await this.liquidityManager.beforeOutInvoicePayment(payAmount, serviceFee)
|
||||||
const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined
|
const provider = use === 'provider' ? this.lnd.liquidProvider.GetProviderDestination() : undefined
|
||||||
const pendingPayment = await this.storage.StartTransaction(async tx => {
|
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 })
|
const op = this.newInvoicePaymentOperation({ invoice, opId, amount: payAmount, networkFee: 0, serviceFee: serviceFee, confirmed: false })
|
||||||
optionals.ack?.(op)
|
optionals.ack?.(op)
|
||||||
try {
|
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)
|
this.storage.paymentStorage.SetExternalPaymentIndex(pendingPayment.serial_id, index)
|
||||||
})
|
})
|
||||||
await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerDst)
|
await this.storage.paymentStorage.UpdateExternalPayment(pendingPayment.serial_id, payment.feeSat, serviceFee, true, payment.providerDst)
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import path from 'path'
|
||||||
export type ServiceFeeSettings = {
|
export type ServiceFeeSettings = {
|
||||||
incomingTxFee: number
|
incomingTxFee: number
|
||||||
outgoingTxFee: number
|
outgoingTxFee: number
|
||||||
incomingAppInvoiceFee: number
|
|
||||||
incomingAppUserInvoiceFee: number
|
|
||||||
outgoingAppInvoiceFee: number
|
outgoingAppInvoiceFee: number
|
||||||
outgoingAppUserInvoiceFee: number
|
outgoingAppUserInvoiceFee: number
|
||||||
outgoingAppUserInvoiceFeeBps: number
|
outgoingAppUserInvoiceFeeBps: number
|
||||||
|
|
@ -15,15 +13,32 @@ export type ServiceFeeSettings = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceFeeSettings => {
|
export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, 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 {
|
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,
|
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,
|
outgoingAppInvoiceFee: chooseEnvInt("OUTGOING_INVOICE_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000,
|
||||||
incomingAppUserInvoiceFee: chooseEnvInt("INCOMING_INVOICE_FEE_USER_BPS", dbEnv, 0, addToDb) / 10000,
|
outgoingAppUserInvoiceFeeBps: serviceFeeBps,
|
||||||
outgoingAppUserInvoiceFeeBps,
|
outgoingAppUserInvoiceFee: serviceFeeBps / 10000,
|
||||||
outgoingAppUserInvoiceFee: outgoingAppUserInvoiceFeeBps / 10000,
|
|
||||||
userToUserFee: chooseEnvInt("TX_FEE_INTERNAL_USER_BPS", dbEnv, 0, addToDb) / 10000,
|
userToUserFee: chooseEnvInt("TX_FEE_INTERNAL_USER_BPS", dbEnv, 0, addToDb) / 10000,
|
||||||
appToUserFee: chooseEnvInt("TX_FEE_INTERNAL_ROOT_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 BTCNetwork = (typeof networks)[number]
|
||||||
export type LndSettings = {
|
export type LndSettings = {
|
||||||
lndLogDir: string
|
lndLogDir: string
|
||||||
outboundFeeFloor: number
|
serviceFeeFloor: number
|
||||||
|
routingFeeLimitBps: number
|
||||||
|
routingFeeFloor: number
|
||||||
mockLnd: boolean
|
mockLnd: boolean
|
||||||
network: BTCNetwork
|
network: BTCNetwork
|
||||||
}
|
}
|
||||||
|
|
@ -112,11 +129,44 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record<string, string | undefi
|
||||||
|
|
||||||
export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => {
|
export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => {
|
||||||
const network = chooseEnv('BTC_NETWORK', dbEnv, 'mainnet', addToDb) as BTCNetwork
|
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 {
|
return {
|
||||||
lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb),
|
lndLogDir: chooseEnv('LND_LOG_DIR', dbEnv, resolveHome("/.lnd/logs/bitcoin/mainnet/lnd.log"), addToDb),
|
||||||
outboundFeeFloor,
|
serviceFeeFloor,
|
||||||
|
routingFeeLimitBps,
|
||||||
|
routingFeeFloor,
|
||||||
mockLnd: false,
|
mockLnd: false,
|
||||||
network: networks.includes(network) ? network : 'mainnet'
|
network: networks.includes(network) ? network : 'mainnet'
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,26 @@ export default class SettingsManager {
|
||||||
for (const key in toAdd) {
|
for (const key in toAdd) {
|
||||||
await this.storage.settingsStorage.setDbEnvIFNeeded(key, toAdd[key])
|
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
|
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 {
|
getStorageSettings(): StorageSettings {
|
||||||
return this.storage.getStorageSettings()
|
return this.storage.getStorageSettings()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue