diff --git a/core/api/src/domain/bitcoin/index.ts b/core/api/src/domain/bitcoin/index.ts index 38044dfde0..038c31eb64 100644 --- a/core/api/src/domain/bitcoin/index.ts +++ b/core/api/src/domain/bitcoin/index.ts @@ -2,7 +2,7 @@ import { InvalidCurrencyBaseAmountError, InvalidSatoshiAmountError, } from "@/domain/errors" -import { MAX_SATS, BtcAmountTooLargeError } from "@/domain/shared" +import { MAX_SATS, BtcAmountTooLargeError, WalletCurrency } from "@/domain/shared" export const SATS_PER_BTC = 10 ** 8 @@ -57,4 +57,7 @@ export const BtcNetwork = { // Offchain routing fees are capped at 0.5% export const FEECAP_BASIS_POINTS = 50n // 100 basis points == 1% -export const FEEMIN = toSats(10) // sats +export const FEECAP_MIN = { + amount: 10n, + currency: WalletCurrency.Btc, +} diff --git a/core/api/src/domain/payments/ln-fees.ts b/core/api/src/domain/payments/ln-fees.ts index 4d2cc28242..22ce30ce2e 100644 --- a/core/api/src/domain/payments/ln-fees.ts +++ b/core/api/src/domain/payments/ln-fees.ts @@ -1,4 +1,4 @@ -import { FEECAP_BASIS_POINTS } from "@/domain/bitcoin" +import { FEECAP_BASIS_POINTS, FEECAP_MIN } from "@/domain/bitcoin" import { MaxFeeTooLargeForRoutelessPaymentError } from "@/domain/bitcoin/lightning" import { WalletCurrency, @@ -14,15 +14,22 @@ const calc = AmountCalculator() export const LnFees = () => { const feeCapBasisPoints = FEECAP_BASIS_POINTS - const maxProtocolAndBankFee = (amount: PaymentAmount) => { - if (amount.amount == 0n) { - return amount - } + const maxProtocolAndBankFee = ( + amount: PaymentAmount, + ): PaymentAmount => { + if (amount.amount === 0n) return amount + + const defaultMaxFee = calc.mulBasisPoints(amount, feeCapBasisPoints) + + const getMaxAmount = (fee: PaymentAmount) => (fee.amount === 0n ? 1n : fee.amount) - const maxFee = calc.mulBasisPoints(amount, feeCapBasisPoints) + const maxFee = + amount.currency === WalletCurrency.Btc + ? getMaxAmount(calc.max(defaultMaxFee, FEECAP_MIN as PaymentAmount)) // allow micro payments + : getMaxAmount(defaultMaxFee) return { - amount: maxFee.amount === 0n ? 1n : maxFee.amount, + amount: maxFee, currency: amount.currency, } } diff --git a/core/api/test/integration/services/lnd-service.spec.ts b/core/api/test/integration/services/lnd-service.spec.ts index 117972ab73..f532d96791 100644 --- a/core/api/test/integration/services/lnd-service.spec.ts +++ b/core/api/test/integration/services/lnd-service.spec.ts @@ -11,7 +11,7 @@ import { import { LND1_PUBKEY, MS_PER_SEC } from "@/config" -import { WalletCurrency } from "@/domain/shared" +import { ONE_SAT, WalletCurrency } from "@/domain/shared" import { toSats } from "@/domain/bitcoin" import { InvoiceNotFoundError, @@ -22,7 +22,6 @@ import { RouteNotFoundError, decodeInvoice, } from "@/domain/bitcoin/lightning" -import { LnFees } from "@/domain/payments" import { LndService } from "@/services/lnd" import { parseLndErrorDetails } from "@/services/lnd/config" @@ -304,7 +303,7 @@ describe("Lnd", () => { const paid = await lndService.payInvoiceViaPaymentDetails({ decodedInvoice: lnInvoice, btcPaymentAmount, - maxFeeAmount: LnFees().maxProtocolAndBankFee(btcPaymentAmount), + maxFeeAmount: ONE_SAT, }) expect(paid).toBeInstanceOf(RouteNotFoundError) }) diff --git a/core/api/test/unit/domain/payments/ln-fees.spec.ts b/core/api/test/unit/domain/payments/ln-fees.spec.ts index 1fe0fe07e7..8f259352f2 100644 --- a/core/api/test/unit/domain/payments/ln-fees.spec.ts +++ b/core/api/test/unit/domain/payments/ln-fees.spec.ts @@ -1,3 +1,4 @@ +import { FEECAP_MIN } from "@/domain/bitcoin" import { AmountCalculator, ONE_CENT, ONE_SAT, WalletCurrency } from "@/domain/shared" import { LnFees, WalletPriceRatio } from "@/domain/payments" import { MaxFeeTooLargeForRoutelessPaymentError } from "@/domain/bitcoin/lightning" @@ -29,13 +30,43 @@ describe("LnFees", () => { }) it("handles a small amount", () => { - const btcAmount = { + let btcAmount = { amount: 1n, currency: WalletCurrency.Btc, } expect(LnFees().maxProtocolAndBankFee(btcAmount)).toEqual({ - amount: 1n, + amount: FEECAP_MIN.amount, + currency: WalletCurrency.Btc, + }) + + btcAmount = { + amount: 1000n, currency: WalletCurrency.Btc, + } + expect(LnFees().maxProtocolAndBankFee(btcAmount)).toEqual({ + amount: FEECAP_MIN.amount, + currency: WalletCurrency.Btc, + }) + + const usdAmount = { + amount: 1n, + currency: WalletCurrency.Usd, + } + expect(LnFees().maxProtocolAndBankFee(usdAmount)).toEqual({ + amount: 1n, + currency: WalletCurrency.Usd, + }) + }) + + it("does not apply FEECAP_MIN to USD amounts", () => { + const usdAmount = { + amount: 1000n, + currency: WalletCurrency.Usd, + } + // With 0.5% fee cap, 1000 cents would result in 5 cents fee + expect(LnFees().maxProtocolAndBankFee(usdAmount)).toEqual({ + amount: 5n, + currency: WalletCurrency.Usd, }) }) }) @@ -179,7 +210,7 @@ describe("LnFees", () => { it("fails for a 1 sat large Btc maxFee", () => { expect( LnFees().verifyMaxFee({ - maxFeeAmount: calc.add(ONE_SAT, ONE_SAT), + maxFeeAmount: calc.add(FEECAP_MIN, ONE_SAT), btcPaymentAmount: ONE_SAT, usdPaymentAmount: ONE_CENT, priceRatio,