Skip to content

Commit 1f93949

Browse files
committed
refactor(core): onhance fee calculation algorithm and structure
1 parent 225daf5 commit 1f93949

File tree

5 files changed

+150
-52
lines changed

5 files changed

+150
-52
lines changed

core/api/src/app/wallets/get-on-chain-fee.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export const getMinerFeeAndPaymentFlow = async <
171171
})
172172
if (minerFee instanceof Error) return minerFee
173173

174-
return builder.withMinerFee(minerFee)
174+
return builder.withMinerFee(minerFee, speed)
175175
}
176176

177177
export const getOnChainFeeForBtcWallet = async <S extends WalletCurrency>(

core/api/src/domain/payments/index.types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,7 @@ type OPFBWithAmount<S extends WalletCurrency, R extends WalletCurrency> = {
328328
type OPFBWithConversion<S extends WalletCurrency, R extends WalletCurrency> = {
329329
withMinerFee(
330330
minerFee: BtcPaymentAmount,
331+
speed: PayoutSpeed,
331332
): Promise<OnChainPaymentFlow<S, R> | ValidationError | DealerPriceServiceError>
332333
withoutMinerFee(): Promise<
333334
OnChainPaymentFlow<S, R> | ValidationError | DealerPriceServiceError

core/api/src/domain/payments/onchain-payment-flow-builder.ts

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,17 @@ import { getFeesConfig } from "@/config"
1313
import {
1414
AmountCalculator,
1515
ONE_CENT,
16-
paymentAmountFromNumber,
1716
ValidationError,
1817
WalletCurrency,
1918
ZERO_BANK_FEE,
2019
} from "@/domain/shared"
2120
import { LessThanDustThresholdError, SelfPaymentError } from "@/domain/errors"
2221
import { OnChainFees, PaymentInitiationMethod, SettlementMethod } from "@/domain/wallets"
23-
import { ImbalanceCalculator } from "@/domain/ledger/imbalance-calculator"
2422

2523
const calc = AmountCalculator()
2624
const feeConfig = getFeesConfig()
2725
const onChainFees = OnChainFees({
28-
feeRatioAsBasisPoints: feeConfig.withdrawRatioAsBasisPoints,
29-
thresholdImbalance: {
30-
amount: BigInt(feeConfig.withdrawThreshold),
31-
currency: WalletCurrency.Btc,
32-
},
26+
onchain: feeConfig.onchain,
3327
})
3428

3529
export const OnChainPaymentFlowBuilder = <S extends WalletCurrency>(
@@ -431,6 +425,7 @@ const OPFBWithConversion = <S extends WalletCurrency, R extends WalletCurrency>(
431425

432426
const withMinerFee = async (
433427
minerFee: BtcPaymentAmount,
428+
speed: PayoutSpeed,
434429
): Promise<OnChainPaymentFlow<S, R> | ValidationError | DealerPriceServiceError> => {
435430
const state = await stateFromPromise(statePromise)
436431
if (state instanceof Error) return state
@@ -441,35 +436,11 @@ const OPFBWithConversion = <S extends WalletCurrency, R extends WalletCurrency>(
441436
})
442437
if (priceRatio instanceof Error) return priceRatio
443438

444-
const minBankFee = paymentAmountFromNumber({
445-
amount: state.senderWithdrawFee || feeConfig.withdrawDefaultMin,
446-
currency: WalletCurrency.Btc,
447-
})
448-
if (minBankFee instanceof Error) return minBankFee
449-
450-
const imbalanceCalculator = ImbalanceCalculator({
451-
method: feeConfig.withdrawMethod,
452-
netInVolumeAmountLightningFn: state.netInVolumeAmountLightningFn,
453-
netInVolumeAmountOnChainFn: state.netInVolumeAmountOnChainFn,
454-
sinceDaysAgo: feeConfig.withdrawDaysLookback,
455-
})
456-
const imbalanceForWallet = await imbalanceCalculator.getSwapOutImbalanceAmount<S>({
457-
id: state.senderWalletId,
458-
currency: state.senderWalletCurrency,
459-
accountId: state.senderAccountId,
460-
})
461-
if (imbalanceForWallet instanceof Error) return imbalanceForWallet
462-
463-
const imbalance =
464-
imbalanceForWallet.currency === WalletCurrency.Btc
465-
? (imbalanceForWallet as BtcPaymentAmount)
466-
: priceRatio.convertFromUsd(imbalanceForWallet as UsdPaymentAmount)
467-
468439
const feeAmounts = onChainFees.withdrawalFee({
469440
minerFee,
470441
amount: state.btcProposedAmount,
471-
minBankFee,
472-
imbalance,
442+
speed,
443+
feeRate: 5,
473444
})
474445

475446
// Calculate amounts & fees

core/api/src/domain/wallets/index.types.d.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,15 +204,14 @@ type DepositFeeCalculator = {
204204
}
205205

206206
type OnchainWithdrawalConfig = {
207-
thresholdImbalance: BtcPaymentAmount
208-
feeRatioAsBasisPoints: bigint
207+
onchain: OnchainFeesConfig
209208
}
210209

211210
type OnChainWithdrawalFeeArgs = {
212211
minerFee: BtcPaymentAmount
213-
minBankFee: BtcPaymentAmount
214-
imbalance: BtcPaymentAmount
215212
amount: BtcPaymentAmount
213+
speed: PayoutSpeed
214+
feeRate: number
216215
}
217216

218217
type WithdrawalFeePriceMethod =
@@ -250,3 +249,29 @@ type PaymentInputValidator = {
250249
args: ValidatePaymentInputArgs<T>,
251250
) => Promise<ValidatePaymentInputRet<T> | ValidationError | RepositoryError>
252251
}
252+
253+
interface TransactionSpecification {
254+
readonly inputCount: number
255+
readonly outputCount: number
256+
}
257+
258+
type InputCountCalculator = (amount: number) => number
259+
type TransactionSizeCalculator = (spec: TransactionSpecification) => number
260+
type DecayRateCalculator = (amount: number, params: FeeDecayCurveParameters) => number
261+
type CostToBankCalculator = (
262+
amount: number,
263+
speed: PayoutSpeed,
264+
feeRate: number,
265+
) => number
266+
type DynamicRateCalculator = (
267+
amount: number,
268+
speed: PayoutSpeed,
269+
feeRate: number,
270+
) => number
271+
type BaseMultiplierCalculator = (speed: PayoutSpeed, feeRate: number) => number
272+
273+
interface FeeDecayCurveParameters {
274+
readonly minRate: number
275+
readonly maxRate: number
276+
readonly divisor: number
277+
}

core/api/src/domain/wallets/withdrawal-fee-calculator.ts

Lines changed: 115 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,139 @@
1-
import { AmountCalculator, ZERO_CENTS, ZERO_SATS } from "@/domain/shared"
1+
import { PayoutSpeed } from "@/domain/bitcoin/onchain"
2+
import { AmountCalculator, WalletCurrency, ZERO_CENTS, ZERO_SATS } from "@/domain/shared"
23

34
const calc = AmountCalculator()
45

56
export const OnChainFees = ({
6-
thresholdImbalance,
7-
feeRatioAsBasisPoints,
7+
onchain,
88
}: OnchainWithdrawalConfig): OnChainFeeCalculator => {
99
const withdrawalFee = ({
1010
minerFee,
1111
amount,
12-
minBankFee,
13-
imbalance,
12+
speed,
13+
feeRate,
1414
}: {
1515
minerFee: BtcPaymentAmount
1616
amount: BtcPaymentAmount
1717
minBankFee: BtcPaymentAmount
18-
imbalance: BtcPaymentAmount
18+
speed: PayoutSpeed
19+
feeRate: number
1920
}) => {
20-
const amountWithImbalanceCalcs = {
21-
amount: imbalance.amount - thresholdImbalance.amount + amount.amount,
22-
currency: amount.currency,
21+
const satoshis = Number(amount.amount)
22+
23+
const dynamicRate = calculateDynamicFeeRate(satoshis, speed, feeRate)
24+
const baseMultiplier = calculateBaseMultiplier(speed, feeRate)
25+
const bankCost = calculateCostToBank(satoshis, speed, feeRate)
26+
27+
const bankFee: BtcPaymentAmount = {
28+
amount: BigInt(Math.round(satoshis * dynamicRate + bankCost * baseMultiplier)),
29+
currency: WalletCurrency.Btc,
2330
}
24-
const baseAmount = calc.max(calc.min(amountWithImbalanceCalcs, amount), ZERO_SATS)
25-
const bankFee = calc.max(
26-
minBankFee,
27-
calc.mulBasisPoints(baseAmount, feeRatioAsBasisPoints),
28-
)
2931

3032
return {
3133
totalFee: calc.add(bankFee, minerFee),
3234
bankFee,
3335
}
3436
}
3537

38+
const createThresholdBasedCalculator =
39+
(
40+
thresholds: readonly { max: number; count: number }[],
41+
defaultCount: number,
42+
): InputCountCalculator =>
43+
(amount: number) => {
44+
const threshold = thresholds.find(({ max }) => amount < max)
45+
return threshold?.count ?? defaultCount
46+
}
47+
48+
const calculateRegularInputCount: InputCountCalculator = createThresholdBasedCalculator(
49+
onchain.thresholds.regular,
50+
onchain.thresholds.defaults.regular,
51+
)
52+
53+
const calculateBatchInputCount: InputCountCalculator = createThresholdBasedCalculator(
54+
onchain.thresholds.batch,
55+
onchain.thresholds.defaults.batch,
56+
)
57+
58+
const calculateTransactionSize: TransactionSizeCalculator = (spec) => {
59+
const { baseSize, inputSize, outputSize } = onchain.transaction
60+
return baseSize + spec.inputCount * inputSize + spec.outputCount * outputSize
61+
}
62+
63+
const calculateExponentialDecay = (
64+
amount: number,
65+
minRate: number,
66+
maxRate: number,
67+
threshold: number,
68+
minAmount: number,
69+
exponentialFactor: number,
70+
): number => {
71+
const span = threshold - minAmount
72+
const exponent = -((amount - minAmount) / span) * exponentialFactor
73+
return minRate + (maxRate - minRate) * Math.exp(exponent)
74+
}
75+
76+
const calculateDecayRate: DecayRateCalculator = (amount, params) => {
77+
const { threshold, minSats, exponentialFactor } = onchain.decayConstants
78+
const { minRate, maxRate, divisor } = params
79+
80+
if (amount < threshold) {
81+
return calculateExponentialDecay(
82+
amount,
83+
minRate,
84+
maxRate,
85+
threshold,
86+
minSats,
87+
exponentialFactor,
88+
)
89+
}
90+
91+
return divisor / amount
92+
}
93+
94+
const calculateNormalizedFactor = (feeRate: number): number => {
95+
const { min, max } = onchain.decayConstants.networkFeeRange
96+
return (feeRate - min) / (max - min)
97+
}
98+
99+
const calculateDynamicFeeRate: DynamicRateCalculator = (amount, speed, feeRate) => {
100+
const { targetRate, ...decayParams } = onchain.decay[speed]
101+
const decay = calculateDecayRate(amount, decayParams)
102+
const normalizedFactor = calculateNormalizedFactor(feeRate)
103+
return decay + normalizedFactor * (targetRate - decay)
104+
}
105+
106+
const calculateBaseMultiplier: BaseMultiplierCalculator = (speed, feeRate) => {
107+
const { factors, offsets } = onchain.multiplier
108+
return factors[speed] / feeRate + offsets[speed]
109+
}
110+
111+
const createTransactionSpec = (
112+
amount: number,
113+
speed: PayoutSpeed,
114+
): TransactionSpecification => {
115+
const { outputs } = onchain.transaction
116+
117+
if (speed === PayoutSpeed.Slow) {
118+
return {
119+
inputCount: calculateBatchInputCount(amount),
120+
outputCount: outputs.batch,
121+
}
122+
}
123+
124+
return {
125+
inputCount: calculateRegularInputCount(amount),
126+
outputCount: outputs.regular,
127+
}
128+
}
129+
130+
const calculateCostToBank: CostToBankCalculator = (amount, speed, feeRate) => {
131+
const spec = createTransactionSpec(amount, speed)
132+
const size = calculateTransactionSize(spec)
133+
134+
return speed === PayoutSpeed.Slow ? Math.round((size * feeRate) / 10) : size * feeRate
135+
}
136+
36137
return {
37138
withdrawalFee,
38139
intraLedgerFees: () => ({

0 commit comments

Comments
 (0)