fixes and cleanup

This commit is contained in:
boufni95 2025-12-18 20:45:47 +00:00
parent 81229b3385
commit 824e98f007
11 changed files with 52 additions and 112 deletions

View file

@ -1169,8 +1169,8 @@ The nostr server will send back a message response, and inside the body there wi
- __invitation_link__: _string_
### CumulativeFees
- __outboundFeeFloor__: _number_
- __serviceFeeBps__: _number_
- __serviceFeeFloor__: _number_
### DebitAuthorization
- __authorized__: _boolean_

View file

@ -230,8 +230,8 @@ type CreateOneTimeInviteLinkResponse struct {
Invitation_link string `json:"invitation_link"`
}
type CumulativeFees struct {
Outboundfeefloor int64 `json:"outboundFeeFloor"`
Servicefeebps int64 `json:"serviceFeeBps"`
Servicefeebps int64 `json:"serviceFeeBps"`
Servicefeefloor int64 `json:"serviceFeeFloor"`
}
type DebitAuthorization struct {
Authorized bool `json:"authorized"`

View file

@ -1303,25 +1303,25 @@ export const CreateOneTimeInviteLinkResponseValidate = (o?: CreateOneTimeInviteL
}
export type CumulativeFees = {
outboundFeeFloor: number
serviceFeeBps: number
serviceFeeFloor: number
}
export const CumulativeFeesOptionalFields: [] = []
export type CumulativeFeesOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
outboundFeeFloor_CustomCheck?: (v: number) => boolean
serviceFeeBps_CustomCheck?: (v: number) => boolean
serviceFeeFloor_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.outboundFeeFloor !== 'number') return new Error(`${path}.outboundFeeFloor: is not a number`)
if (opts.outboundFeeFloor_CustomCheck && !opts.outboundFeeFloor_CustomCheck(o.outboundFeeFloor)) return new Error(`${path}.outboundFeeFloor: 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`)
if (typeof o.serviceFeeFloor !== 'number') return new Error(`${path}.serviceFeeFloor: is not a number`)
if (opts.serviceFeeFloor_CustomCheck && !opts.serviceFeeFloor_CustomCheck(o.serviceFeeFloor)) return new Error(`${path}.serviceFeeFloor: custom check failed`)
return null
}

View file

@ -857,7 +857,7 @@ message SwapsList {
}
message CumulativeFees {
int64 outboundFeeFloor = 2;
int64 serviceFeeFloor = 2;
int64 serviceFeeBps = 3;
}

View file

@ -63,7 +63,7 @@ export default class {
this.htlcCb = htlcCb
this.channelEventCb = channelEventCb
this.liquidProvider = liquidProvider
// Skip LND client initialization if using only liquidity provider
if (liquidProvider.getSettings().useOnlyLiquidityProvider) {
this.log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping LND client initialization")
@ -79,7 +79,7 @@ export default class {
this.walletKit = new WalletKitClient(dummyTransport)
return
}
const { lndAddr, lndCertPath, lndMacaroonPath } = this.getSettings().lndNodeSettings
const lndCert = fs.readFileSync(lndCertPath);
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
@ -392,7 +392,7 @@ export default class {
const decoded = decodeBolt11(paymentRequest)
let numSatoshis = 0
let paymentHash = ''
for (const section of decoded.sections) {
if (section.name === 'amount') {
// Amount is in millisatoshis
@ -401,11 +401,11 @@ export default class {
paymentHash = section.value as string
}
}
if (!paymentHash) {
throw new Error("Payment hash not found in invoice")
}
return { numSatoshis, paymentHash }
} catch (err: any) {
throw new Error(`Failed to decode invoice: ${err.message}`)
@ -434,7 +434,7 @@ export default class {
// Force use of provider when bypass is enabled
const mustUseProvider = this.liquidProvider.getSettings().useOnlyLiquidityProvider || useProvider
if (mustUseProvider) {
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.service_fee, valueSat: res.amount_paid, paymentPreimage: res.preimage, providerDst }
}

View file

@ -69,14 +69,14 @@ 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, outboundFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats)
const { max, serviceFeeFloor, 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: 0,
network_max_fee_fixed: outboundFeeFloor,
network_max_fee_fixed: serviceFeeFloor,
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] }),

View file

@ -154,7 +154,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, outboundFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats)
const { max, serviceFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats)
return {
identifier: u.identifier,
info: {
@ -163,7 +163,7 @@ export default class {
max_withdrawable: max,
user_identifier: u.identifier,
network_max_fee_bps: 0,
network_max_fee_fixed: outboundFeeFloor,
network_max_fee_fixed: serviceFeeFloor,
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] }),
@ -214,14 +214,14 @@ 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, outboundFeeFloor, serviceFeeBps } = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats)
const { max, serviceFeeFloor, 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: 0,
network_max_fee_fixed: outboundFeeFloor,
network_max_fee_fixed: serviceFeeFloor,
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] }),

View file

@ -130,7 +130,7 @@ export class LiquidityProvider {
return res
}
this.feesCache = {
outboundFeeFloor: res.network_max_fee_fixed,
serviceFeeFloor: res.network_max_fee_fixed,
serviceFeeBps: res.service_fee_bps
}
this.latestReceivedBalance = res.balance
@ -151,11 +151,11 @@ export class LiquidityProvider {
return 0
}
const balance = this.latestReceivedBalance
const { outboundFeeFloor, serviceFeeBps } = this.feesCache
const { serviceFeeFloor, serviceFeeBps } = this.feesCache
const div = 1 + (serviceFeeBps / 10000)
const maxWithoutFixed = Math.floor(balance / div)
const fee = balance - maxWithoutFixed
return balance - Math.max(fee, outboundFeeFloor)
return balance - Math.max(fee, serviceFeeFloor)
}
GetLatestBalance = () => {
@ -173,7 +173,7 @@ export class LiquidityProvider {
const fees = f ? f : this.GetFees()
const serviceFeeRate = fees.serviceFeeBps / 10000
const serviceFee = Math.ceil(serviceFeeRate * amount)
return Math.max(serviceFee, fees.outboundFeeFloor)
return Math.max(serviceFee, fees.serviceFeeFloor)
}
CanProviderPay = async (amount: number, localServiceFee: number): Promise<boolean> => {
@ -215,13 +215,16 @@ 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, disabled or unreachable")
}
const fees = this.GetFees()
const providerServiceFee = this.GetServiceFee(decodedAmount, fees)
if (feeLimit && providerServiceFee > feeLimit) {
throw new Error("provider service fee is greater than the fee limit")
}
this.pendingPayments[invoice] = decodedAmount + providerServiceFee
const timeout = setTimeout(() => {
if (!this.pendingPaymentsAck[invoice]) {

View file

@ -173,7 +173,7 @@ export default class {
getReceiveServiceFee = (action: Types.UserOperationType, amount: number, managedUser: boolean): number => {
switch (action) {
case Types.UserOperationType.INCOMING_TX:
return Math.ceil(this.settings.getSettings().serviceFeeSettings.incomingTxFee * amount)
return 0
case Types.UserOperationType.INCOMING_INVOICE:
// Incoming invoice fees are always 0 (not configurable)
return 0
@ -197,18 +197,14 @@ export default class {
getSendServiceFee = (action: Types.UserOperationType, amount: number, managedUser: boolean): number => {
switch (action) {
case Types.UserOperationType.OUTGOING_TX:
// Internal address payment, treat like user-to-user
if (managedUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
}
return Math.ceil(this.settings.getSettings().serviceFeeSettings.rootToUserFee * amount)
throw new Error("OUTGOING_TX is not a valid send service fee action")
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)
return Math.max(fee, this.settings.getSettings().serviceFeeSettings.serviceFeeFloor)
case Types.UserOperationType.OUTGOING_USER_TO_USER:
if (managedUser) {
return Math.ceil(this.settings.getSettings().serviceFeeSettings.userToUserFee * amount)
@ -272,18 +268,17 @@ export default class {
}
GetFees = (): Types.CumulativeFees => {
const { serviceFeeBps } = this.settings.getSettings().serviceFeeSettings
const { serviceFeeFloor } = this.settings.getSettings().lndSettings
return { outboundFeeFloor: serviceFeeFloor, serviceFeeBps: serviceFeeBps }
const { serviceFeeBps, serviceFeeFloor } = this.settings.getSettings().serviceFeeSettings
return { serviceFeeFloor, serviceFeeBps }
}
GetMaxPayableInvoice(balance: number): Types.CumulativeFees & { max: number } {
const { outboundFeeFloor, serviceFeeBps } = this.GetFees()
const { serviceFeeFloor, serviceFeeBps } = this.GetFees()
const div = 1 + (serviceFeeBps / 10000)
const maxWithoutFixed = Math.floor(balance / div)
const fee = balance - maxWithoutFixed
const max = balance - Math.max(fee, outboundFeeFloor)
return { max, outboundFeeFloor, serviceFeeBps }
const max = balance - Math.max(fee, serviceFeeFloor)
return { max, serviceFeeFloor, serviceFeeBps }
}
async DecodeInvoice(req: Types.DecodeInvoiceRequest): Promise<Types.DecodeInvoiceResponse> {
const decoded = await this.lnd.DecodeInvoice(req.invoice)
@ -299,10 +294,10 @@ export default class {
throw new Error("user is banned, cannot send payment")
}
if (req.expected_fees) {
const { outboundFeeFloor, serviceFeeBps } = req.expected_fees
const serviceFixed = this.settings.getSettings().lndSettings.serviceFeeFloor
const { serviceFeeFloor, serviceFeeBps } = req.expected_fees
const serviceFixed = this.settings.getSettings().serviceFeeSettings.serviceFeeFloor
const serviceBps = this.settings.getSettings().serviceFeeSettings.serviceFeeBps
if (serviceFixed !== outboundFeeFloor || serviceBps !== serviceFeeBps) {
if (serviceFixed !== serviceFeeFloor || serviceBps !== serviceFeeBps) {
throw new Error("fees do not match the expected fees")
}
}
@ -522,21 +517,11 @@ export default class {
this.swaps.reverseSwaps.SubscribeToTransactionSwap(data, result => {
swapResult = result
})
// Validate that the invoice amount matches what was quoted
const decoded = await this.lnd.DecodeInvoice(txSwap.invoice)
if (decoded.numSatoshis !== txSwap.invoice_amount) {
throw new Error("swap invoice amount does not match quote")
}
const fees = this.GetFees()
let payment: Types.PayInvoiceResponse
try {
payment = await this.PayInvoice(ctx.user_id, {
amount: 0,
invoice: txSwap.invoice,
expected_fees: {
outboundFeeFloor: fees.outboundFeeFloor,
serviceFeeBps: fees.serviceFeeBps
}
invoice: txSwap.invoice
}, app, { swapOperationId: req.swap_operation_id })
if (!swapResult.ok) {
this.log("invoice payment successful, but swap failed")
@ -572,7 +557,7 @@ export default class {
const { blockHeight } = await this.lnd.GetInfo()
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const isManagedUser = ctx.user_id !== app.owner.user_id
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, isManagedUser)
const serviceFee = this.getSendServiceFee(Types.UserOperationType.OUTGOING_USER_TO_USER, req.amoutSats, isManagedUser)
const txId = crypto.randomBytes(32).toString("hex")
const addressData = `${req.address}:${txId}`

View file

@ -3,38 +3,22 @@ import os from 'os'
import path from 'path'
export type ServiceFeeSettings = {
incomingTxFee: number
serviceFee: number
serviceFeeBps: number
serviceFeeFloor: number
userToUserFee: number
rootToUserFee: number
}
export const LoadServiceFeeSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): ServiceFeeSettings => {
// 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)
}
}
const oldServiceFeeBps = chooseEnvInt("OUTGOING_INVOICE_FEE_USER_BPS", dbEnv, 60, addToDb)
const serviceFeeBps = chooseEnvInt("SERVICE_FEE_BPS", dbEnv, oldServiceFeeBps, addToDb)
const oldRoutingFeeFloor = chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 10, addToDb)
const serviceFeeFloor = chooseEnvInt("SERVICE_FEE_FLOOR_SATS", dbEnv, oldRoutingFeeFloor, addToDb)
return {
incomingTxFee: 0, // Not configurable, always 0
serviceFeeBps: serviceFeeBps,
serviceFeeBps,
serviceFee: serviceFeeBps / 10000,
serviceFeeFloor,
userToUserFee: chooseEnvInt("TX_FEE_INTERNAL_USER_BPS", dbEnv, 0, addToDb) / 10000,
rootToUserFee: chooseEnvInt("TX_FEE_INTERNAL_ROOT_BPS", dbEnv, 0, addToDb) / 10000,
}
@ -91,7 +75,6 @@ const networks = ['mainnet', 'testnet', 'regtest'] as const
export type BTCNetwork = (typeof networks)[number]
export type LndSettings = {
lndLogDir: string
serviceFeeFloor: number
routingFeeLimitBps: number
routingFeeFloor: number
mockLnd: boolean
@ -125,42 +108,11 @@ export const LoadLndNodeSettingsFromEnv = (dbEnv: Record<string, string | undefi
export const LoadLndSettingsFromEnv = (dbEnv: Record<string, string | undefined>, addToDb?: EnvCacher): LndSettings => {
const network = chooseEnv('BTC_NETWORK', dbEnv, 'mainnet', addToDb) as BTCNetwork
// 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 oldRoutingFeeFloor = chooseEnvInt('OUTBOUND_MAX_FEE_EXTRA_SATS', dbEnv, 5, addToDb)
const routingFeeFloor = chooseEnvInt('ROUTING_FEE_FLOOR_SATS', dbEnv, oldRoutingFeeFloor, 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),
serviceFeeFloor,
routingFeeLimitBps,
routingFeeFloor,
mockLnd: false,

View file

@ -59,7 +59,7 @@ export default class SettingsManager {
const { serviceFeeSettings, lndSettings } = settings
const serviceFeeBps = serviceFeeSettings.serviceFeeBps
const routingFeeLimitBps = lndSettings.routingFeeLimitBps
const serviceFeeFloor = lndSettings.serviceFeeFloor
const serviceFeeFloor = serviceFeeSettings.serviceFeeFloor
const routingFeeFloor = lndSettings.routingFeeFloor
if (routingFeeLimitBps > serviceFeeBps) {