This commit is contained in:
shocknet-justin 2025-12-18 01:20:46 -05:00
parent ad8cd91aad
commit efa3976657
3 changed files with 46 additions and 41 deletions

View file

@ -170,7 +170,7 @@ export default class {
} }
} }
getReceiveServiceFee = (action: Types.UserOperationType, amount: number, appUser: boolean): number => { getReceiveServiceFee = (action: Types.UserOperationType, amount: number, managedUser: boolean): number => {
switch (action) { switch (action) {
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)
@ -178,34 +178,42 @@ export default class {
// Incoming invoice fees are always 0 (not configurable) // Incoming invoice fees are always 0 (not configurable)
return 0 return 0
case Types.UserOperationType.INCOMING_USER_TO_USER: case Types.UserOperationType.INCOMING_USER_TO_USER:
if (appUser) { if (managedUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
} }
return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.rootToUserFee * amount)
default: default:
throw new Error("Unknown receive action type") throw new Error("Unknown receive action type")
} }
} }
getInvoicePaymentServiceFee = (amount: number, appUser: boolean): number => { getInvoicePaymentServiceFee = (amount: number, managedUser: boolean): number => {
if (appUser) { if (!managedUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFee * amount) return 0 // Root doesn't pay service fee to themselves
} }
return Math.ceil(this.settings.getSettings().serviceFeeSettings.outgoingAppInvoiceFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.serviceFee * amount)
} }
getSendServiceFee = (action: Types.UserOperationType, amount: number, appUser: boolean): number => { getSendServiceFee = (action: Types.UserOperationType, amount: number, managedUser: boolean): number => {
switch (action) { switch (action) {
case Types.UserOperationType.OUTGOING_TX: case Types.UserOperationType.OUTGOING_TX:
throw new Error("Sending a transaction is not supported") // Internal address payment, treat like user-to-user
case Types.UserOperationType.OUTGOING_INVOICE: if (managedUser) {
const fee = this.getInvoicePaymentServiceFee(amount, appUser)
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) return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
} }
return Math.ceil(this.settings.getSettings().serviceFeeSettings.appToUserFee * amount) return Math.ceil(this.settings.getSettings().serviceFeeSettings.rootToUserFee * amount)
case Types.UserOperationType.OUTGOING_INVOICE:
const fee = this.getInvoicePaymentServiceFee(amount, managedUser)
// Only managed users pay the service fee floor
if (!managedUser) {
return 0
}
return Math.max(fee, this.settings.getSettings().lndSettings.serviceFeeFloor)
case Types.UserOperationType.OUTGOING_USER_TO_USER:
if (managedUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
}
return Math.ceil(this.settings.getSettings().serviceFeeSettings.rootToUserFee * amount)
default: default:
throw new Error("Unknown service action type") throw new Error("Unknown service action type")
} }
@ -264,9 +272,9 @@ export default class {
} }
GetFees = (): Types.CumulativeFees => { GetFees = (): Types.CumulativeFees => {
const { outgoingAppUserInvoiceFeeBps } = this.settings.getSettings().serviceFeeSettings const { serviceFeeBps } = this.settings.getSettings().serviceFeeSettings
const { serviceFeeFloor } = this.settings.getSettings().lndSettings const { serviceFeeFloor } = this.settings.getSettings().lndSettings
return { outboundFeeFloor: serviceFeeFloor, serviceFeeBps: outgoingAppUserInvoiceFeeBps } return { outboundFeeFloor: serviceFeeFloor, serviceFeeBps: serviceFeeBps }
} }
GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } { GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
@ -293,7 +301,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.serviceFeeFloor const serviceFixed = this.settings.getSettings().lndSettings.serviceFeeFloor
const serviceBps = this.settings.getSettings().serviceFeeSettings.outgoingAppUserInvoiceFeeBps const serviceBps = this.settings.getSettings().serviceFeeSettings.serviceFeeBps
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")
} }
@ -306,8 +314,8 @@ export default class {
throw new Error("invoice has no value, an amount must be provided in the request") throw new Error("invoice has no value, an amount must be provided in the request")
} }
const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis) const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis)
const isAppUserPayment = userId !== linkedApplication.owner.user_id const isManagedUser = userId !== linkedApplication.owner.user_id
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isAppUserPayment) const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount, isManagedUser)
const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice) const internalInvoice = await this.storage.paymentStorage.GetInvoiceOwner(req.invoice)
if (internalInvoice && internalInvoice.paid_at_unix > 0) { if (internalInvoice && internalInvoice.paid_at_unix > 0) {
throw new Error("this invoice was already paid") throw new Error("this invoice was already paid")
@ -333,7 +341,7 @@ export default class {
throw err throw err
} }
const feeDiff = serviceFee - paymentInfo.networkFee const feeDiff = serviceFee - paymentInfo.networkFee
if (isAppUserPayment && feeDiff > 0) { if (isManagedUser && feeDiff > 0) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, feeDiff, "fees") await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, feeDiff, "fees")
} }
const user = await this.storage.userStorage.GetUser(userId) const user = await this.storage.userStorage.GetUser(userId)
@ -431,8 +439,8 @@ export default class {
const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice) const decoded = await this.lnd.DecodeInvoice(res.createdResponse.invoice)
const swapFee = decoded.numSatoshis - chainTotal const swapFee = decoded.numSatoshis - chainTotal
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isAppUserPayment = ctx.user_id !== app.owner.user_id const isManagedUser = ctx.user_id !== app.owner.user_id
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isAppUserPayment) const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, decoded.numSatoshis, isManagedUser)
const newSwap = await this.storage.paymentStorage.AddTransactionSwap({ const newSwap = await this.storage.paymentStorage.AddTransactionSwap({
app_user_id: ctx.app_user_id, app_user_id: ctx.app_user_id,
swap_quote_id: res.createdResponse.id, swap_quote_id: res.createdResponse.id,
@ -546,14 +554,14 @@ export default class {
} }
const { blockHeight } = await this.lnd.GetInfo() const { blockHeight } = await this.lnd.GetInfo()
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false) const isManagedUser = ctx.user_id !== app.owner.user_id
const isAppUserPayment = ctx.user_id !== app.owner.user_id const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, isManagedUser)
const txId = crypto.randomBytes(32).toString("hex") const txId = crypto.randomBytes(32).toString("hex")
const addressData = `${req.address}:${txId}` const addressData = `${req.address}:${txId}`
await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData) await this.storage.userStorage.DecrementUserBalance(ctx.user_id, req.amoutSats + serviceFee, addressData)
this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, 'internal') this.addressPaidCb({ hash: txId, index: 0 }, req.address, req.amoutSats, 'internal')
if (isAppUserPayment && serviceFee > 0) { if (isManagedUser && serviceFee > 0) {
await this.storage.userStorage.IncrementUserBalance(app.owner.user_id, serviceFee, 'fees') await this.storage.userStorage.IncrementUserBalance(app.owner.user_id, serviceFee, 'fees')
} }
const chainFees = 0 const chainFees = 0
@ -586,7 +594,8 @@ export default class {
} }
}), }),
quotes: pendingSwaps.map(s => { quotes: pendingSwaps.map(s => {
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, s.invoice_amount, true) const isManagedUser = true // ListSwaps is only called by app users
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_INVOICE, s.invoice_amount, isManagedUser)
return { return {
swap_operation_id: s.swap_operation_id, swap_operation_id: s.swap_operation_id,
invoice_amount_sats: s.invoice_amount, invoice_amount_sats: s.invoice_amount,
@ -922,14 +931,14 @@ export default class {
if (fromUser.balance_sats < amount) { if (fromUser.balance_sats < amount) {
throw new Error("not enough balance to send payment") throw new Error("not enough balance to send payment")
} }
const isAppUserPayment = fromUser.user_id !== linkedApplication.owner.user_id const isManagedUser = fromUser.user_id !== linkedApplication.owner.user_id
let fee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isAppUserPayment) let fee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, amount, isManagedUser)
const toDecrement = amount + fee const toDecrement = amount + fee
const paymentEntry = await this.storage.paymentStorage.AddPendingUserToUserPayment(fromUserId, toUserId, amount, fee, linkedApplication, tx) 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) await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, toDecrement, `${toUserId}:${paymentEntry.serial_id}`, tx)
await this.storage.userStorage.IncrementUserBalance(toUser.user_id, amount, `${fromUserId}:${paymentEntry.serial_id}`, tx) await this.storage.userStorage.IncrementUserBalance(toUser.user_id, amount, `${fromUserId}:${paymentEntry.serial_id}`, tx)
await this.storage.paymentStorage.SetPendingUserToUserPaymentAsPaid(paymentEntry.serial_id, tx) await this.storage.paymentStorage.SetPendingUserToUserPaymentAsPaid(paymentEntry.serial_id, tx)
if (isAppUserPayment && fee > 0) { if (isManagedUser && fee > 0) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee, 'fees', tx) await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee, 'fees', tx)
} }
return paymentEntry return paymentEntry

View file

@ -4,12 +4,10 @@ import path from 'path'
export type ServiceFeeSettings = { export type ServiceFeeSettings = {
incomingTxFee: number incomingTxFee: number
outgoingTxFee: number serviceFee: number
outgoingAppInvoiceFee: number serviceFeeBps: number
outgoingAppUserInvoiceFee: number
outgoingAppUserInvoiceFeeBps: number
userToUserFee: number userToUserFee: number
appToUserFee: number rootToUserFee: number
} }
export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceFeeSettings => { export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceFeeSettings => {
@ -35,12 +33,10 @@ export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | und
} }
return { return {
incomingTxFee: 0, // Not configurable, always 0 incomingTxFee: 0, // Not configurable, always 0
outgoingTxFee: chooseEnvInt("OUTGOING_CHAIN_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000, serviceFeeBps: serviceFeeBps,
outgoingAppInvoiceFee: chooseEnvInt("OUTGOING_INVOICE_FEE_ROOT_BPS", dbEnv, 60, addToDb) / 10000, serviceFee: serviceFeeBps / 10000,
outgoingAppUserInvoiceFeeBps: serviceFeeBps,
outgoingAppUserInvoiceFee: serviceFeeBps / 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, rootToUserFee: chooseEnvInt("TX_FEE_INTERNAL_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
} }
} }

View file

@ -57,7 +57,7 @@ export default class SettingsManager {
private validateFeeSettings(settings: FullSettings): void { private validateFeeSettings(settings: FullSettings): void {
const { serviceFeeSettings, lndSettings } = settings const { serviceFeeSettings, lndSettings } = settings
const serviceFeeBps = serviceFeeSettings.outgoingAppUserInvoiceFeeBps const serviceFeeBps = serviceFeeSettings.serviceFeeBps
const routingFeeLimitBps = lndSettings.routingFeeLimitBps const routingFeeLimitBps = lndSettings.routingFeeLimitBps
const serviceFeeFloor = lndSettings.serviceFeeFloor const serviceFeeFloor = lndSettings.serviceFeeFloor
const routingFeeFloor = lndSettings.routingFeeFloor const routingFeeFloor = lndSettings.routingFeeFloor