mirror of
https://github.com/muun/recovery.git
synced 2025-11-12 06:50:18 -05:00
563 lines
19 KiB
Go
563 lines
19 KiB
Go
package operation
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/muun/libwallet/fees"
|
|
)
|
|
|
|
// paymentAnalyzer is the component that decides whether a payment can be made or not, and what
|
|
// are the amount and fees involved. Here a list of some important design decisions:
|
|
//
|
|
// - Delegate to the caller the need to make another call if she wants to analyze with minimumFee
|
|
// - 2 entry points: toAddress for onchain payments, toInvoice for offchain payments
|
|
// - PaymentAnalyzer return errors only on programmer errors, not for "payment errors" aka
|
|
// situations that can happen like amount greater than balance
|
|
// - Returned PaymentAnalysis contains a status that represent:
|
|
// - Success → `StatusOk`
|
|
// - Unrecoverable error -> `StatusAmountGreaterThanBalance`, `StatusAmountTooSmall`
|
|
// - Maybe recoverable error → `StatusUnpayable`
|
|
// - A status error is:
|
|
// - Unrecoverable: if there's no way to pay the specified amount with the current state
|
|
// - Recoverable: if there's a way to pay the specified amount with the current state
|
|
// (e.g changing the fee, if it applies)
|
|
// - `OutputAmount > uxtoBalance` IS NOT A `StatusAmountGreaterThanBalance`, because the problem
|
|
// is not that the `amount > totalBalance` but that the swapFees (sweep + lightning) and/or
|
|
// collectAmount make `OutputAmount > uxtoBalance`. Hence it is an `StatusUnpayable`.
|
|
// - We don't allow TFFA for LEND swaps (don't want to lend money if you're taking it all away)
|
|
// - FeeCalculator DOES NOT return error when amount > balance
|
|
// - Instead we return the fee it would take to spend all utxos and delegate to the caller the
|
|
// task of checking if that is spendable with the given amount
|
|
// - This is to avoid using go error handling
|
|
// - We finally renamed sweepFee to outputPadding since that's its only use. Here's how it works:
|
|
// - Only makes sense for swaps
|
|
// - If amount ≥ DUST ⇒ No need for padding (outputPadding = 0)
|
|
// - If amount < DUST & debt < maxDebt ⇒ We use debt as padding (outputPadding = 0)
|
|
// - If amount < DUST && debt >= maxDebt ⇒ outputPadding = DUST - amount
|
|
// - If amount < DUST and TFFA
|
|
// - Can't use debt as padding (don't lend money for TFFA)
|
|
// - Can't use fee as we are using all funds and they are < DUST
|
|
// - If there's need for padding ⇒ payment is UNPAYABLE
|
|
// - Hence, outputPadding = 0 or payment is UNPAYABLE
|
|
|
|
const DustThreshold = 546
|
|
|
|
type PaymentToAddress struct {
|
|
TakeFeeFromAmount bool
|
|
AmountInSat int64
|
|
FeeRateInSatsPerVByte float64
|
|
}
|
|
|
|
type PaymentToInvoice struct {
|
|
TakeFeeFromAmount bool
|
|
AmountInSat int64
|
|
SwapFees *fees.SwapFees // Nullable before we know the paymentAmount for amountless invoice
|
|
BestRouteFees []fees.BestRouteFees // Nullable when we know the amount beforehand (invoice with amount)
|
|
FundingOutputPolicies *fees.FundingOutputPolicies // Nullable when we know the amount beforehand (invoice with amount)
|
|
}
|
|
|
|
type PaymentAnalyzer struct {
|
|
feeWindow *FeeWindow
|
|
nextTransactionSize *NextTransactionSize
|
|
feeCalculator *feeCalculator
|
|
}
|
|
|
|
type SizeForAmount struct {
|
|
AmountInSat int64
|
|
SizeInVByte int64
|
|
}
|
|
|
|
type NextTransactionSize struct {
|
|
SizeProgression []SizeForAmount
|
|
ExpectedDebtInSat int64
|
|
}
|
|
|
|
func (nts *NextTransactionSize) UtxoBalance() int64 {
|
|
if len(nts.SizeProgression) == 0 {
|
|
return 0
|
|
}
|
|
return nts.SizeProgression[len(nts.SizeProgression)-1].AmountInSat
|
|
}
|
|
|
|
func (nts *NextTransactionSize) TotalBalance() int64 {
|
|
return nts.UtxoBalance() - nts.ExpectedDebtInSat
|
|
}
|
|
|
|
type AnalysisStatus string
|
|
|
|
const (
|
|
AnalysisStatusOk AnalysisStatus = "Ok"
|
|
AnalysisStatusAmountGreaterThanBalance AnalysisStatus = "GreaterThanBalance"
|
|
AnalysisStatusAmountTooSmall AnalysisStatus = "AmountTooSmall"
|
|
AnalysisStatusUnpayable AnalysisStatus = "Unpayable"
|
|
)
|
|
|
|
type PaymentAnalysis struct {
|
|
Status AnalysisStatus
|
|
AmountInSat int64
|
|
FeeInSat int64
|
|
SwapFees *fees.SwapFees
|
|
TotalInSat int64
|
|
}
|
|
|
|
func NewPaymentAnalyzer(feeWindow *FeeWindow, nts *NextTransactionSize) *PaymentAnalyzer {
|
|
return &PaymentAnalyzer{
|
|
feeWindow: feeWindow,
|
|
nextTransactionSize: nts,
|
|
feeCalculator: &feeCalculator{nts},
|
|
}
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) totalBalance() int64 {
|
|
return a.nextTransactionSize.TotalBalance()
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) utxoBalance() int64 {
|
|
return a.nextTransactionSize.UtxoBalance()
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) ToAddress(payment *PaymentToAddress) (*PaymentAnalysis, error) {
|
|
if payment.AmountInSat < DustThreshold {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusAmountTooSmall,
|
|
}, nil
|
|
}
|
|
if payment.AmountInSat > a.totalBalance() {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusAmountGreaterThanBalance,
|
|
TotalInSat: payment.AmountInSat,
|
|
}, nil
|
|
}
|
|
if payment.TakeFeeFromAmount && payment.AmountInSat != a.totalBalance() {
|
|
return nil, fmt.Errorf("amount (%v) != userBalance (%v) for TFFA", payment.AmountInSat, a.totalBalance())
|
|
}
|
|
|
|
if payment.TakeFeeFromAmount {
|
|
return a.analyzeFeeFromAmount(payment)
|
|
}
|
|
return a.analyzeFeeFromRemainingBalance(payment)
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) analyzeFeeFromAmount(payment *PaymentToAddress) (*PaymentAnalysis, error) {
|
|
fee := a.feeCalculator.Fee(payment.AmountInSat, payment.FeeRateInSatsPerVByte, true)
|
|
|
|
total := payment.AmountInSat
|
|
amount := total - fee
|
|
|
|
if amount <= DustThreshold {
|
|
// avoid returning a negative amount
|
|
if amount < 0 {
|
|
amount = 0
|
|
}
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
AmountInSat: amount,
|
|
TotalInSat: payment.AmountInSat,
|
|
FeeInSat: fee,
|
|
}, nil
|
|
}
|
|
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusOk,
|
|
AmountInSat: amount,
|
|
TotalInSat: payment.AmountInSat,
|
|
FeeInSat: fee,
|
|
}, nil
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) analyzeFeeFromRemainingBalance(payment *PaymentToAddress) (*PaymentAnalysis, error) {
|
|
fee := a.feeCalculator.Fee(payment.AmountInSat, payment.FeeRateInSatsPerVByte, false)
|
|
total := payment.AmountInSat + fee
|
|
|
|
if total > a.totalBalance() {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
AmountInSat: payment.AmountInSat,
|
|
FeeInSat: fee,
|
|
TotalInSat: total,
|
|
}, nil
|
|
}
|
|
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusOk,
|
|
AmountInSat: payment.AmountInSat,
|
|
TotalInSat: total,
|
|
FeeInSat: fee,
|
|
}, nil
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) ToInvoice(payment *PaymentToInvoice) (*PaymentAnalysis, error) {
|
|
if payment.AmountInSat <= 0 {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusAmountTooSmall,
|
|
}, nil
|
|
}
|
|
if payment.AmountInSat > a.totalBalance() {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusAmountGreaterThanBalance,
|
|
TotalInSat: payment.AmountInSat,
|
|
}, nil
|
|
}
|
|
if payment.TakeFeeFromAmount {
|
|
if payment.BestRouteFees == nil {
|
|
return nil, errors.New("fixed amount swap can't be TFFA since that would change the amount")
|
|
}
|
|
if payment.AmountInSat != a.totalBalance() {
|
|
return nil, fmt.Errorf("amount (%v) != userBalance (%v) for TFFA", payment.AmountInSat, a.totalBalance())
|
|
}
|
|
}
|
|
|
|
if payment.BestRouteFees != nil {
|
|
|
|
// As users can enter newOp screen with 0 balance, we need to check for amount == 0
|
|
// because of our rule (if balance == amount then useAllFunds = true)
|
|
if !payment.TakeFeeFromAmount || payment.AmountInSat == 0 {
|
|
// User chose a specific amount
|
|
return a.analyzeFixedAmountSwap(payment, fees.ComputeSwapFees(
|
|
btcutil.Amount(payment.AmountInSat),
|
|
payment.BestRouteFees,
|
|
payment.FundingOutputPolicies,
|
|
payment.TakeFeeFromAmount,
|
|
))
|
|
|
|
} else {
|
|
return a.analyzeTFFAAmountlessInvoiceSwap(payment)
|
|
}
|
|
}
|
|
|
|
if payment.SwapFees == nil {
|
|
return nil, fmt.Errorf("payment is missing required swap fees data")
|
|
}
|
|
|
|
return a.analyzeFixedAmountSwap(payment, payment.SwapFees)
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) analyzeFixedAmountSwap(payment *PaymentToInvoice, swapFees *fees.SwapFees) (*PaymentAnalysis, error) {
|
|
switch swapFees.DebtType {
|
|
case fees.DebtTypeLend:
|
|
return a.analyzeLendSwap(payment, swapFees)
|
|
case fees.DebtTypeCollect:
|
|
fallthrough
|
|
case fees.DebtTypeNone:
|
|
return a.analyzeCollectSwap(payment, swapFees) // a non-debt swap is just a collect swap with debtAmount = 0
|
|
}
|
|
return nil, fmt.Errorf("unsupported debt type: %v", swapFees.DebtType)
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) analyzeLendSwap(payment *PaymentToInvoice, swapFees *fees.SwapFees) (*PaymentAnalysis, error) {
|
|
|
|
amount := payment.AmountInSat
|
|
total := amount + int64(swapFees.RoutingFee)
|
|
|
|
if total > a.totalBalance() {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
AmountInSat: amount,
|
|
TotalInSat: total,
|
|
FeeInSat: 0,
|
|
SwapFees: swapFees,
|
|
}, nil
|
|
}
|
|
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusOk,
|
|
AmountInSat: amount,
|
|
TotalInSat: total,
|
|
FeeInSat: 0,
|
|
SwapFees: swapFees,
|
|
}, nil
|
|
}
|
|
|
|
// Analyze non LEND swaps (e.g both COLLECT and NON-DEBT swaps), understanding that both cases warrant
|
|
// the same analysis. A non-debt swap is just a collect swap with debtAmount = 0.
|
|
func (a *PaymentAnalyzer) analyzeCollectSwap(payment *PaymentToInvoice, swapFees *fees.SwapFees) (*PaymentAnalysis, error) {
|
|
|
|
outputAmount := int64(swapFees.OutputAmount)
|
|
collectAmount := int64(swapFees.DebtAmount)
|
|
|
|
expectedOutputAmount := payment.AmountInSat +
|
|
int64(swapFees.RoutingFee) +
|
|
int64(swapFees.OutputPadding) +
|
|
collectAmount
|
|
|
|
if outputAmount != expectedOutputAmount {
|
|
return nil, fmt.Errorf(
|
|
"swap integrity check failed (outputAmount=%v, original_amount=%v, routing_fee=%v, output_padding=%v, collect_amount=%v)",
|
|
outputAmount,
|
|
payment.AmountInSat,
|
|
int64(swapFees.RoutingFee),
|
|
int64(swapFees.OutputPadding),
|
|
collectAmount,
|
|
)
|
|
}
|
|
|
|
feeRate, err := a.feeWindow.SwapFeeRate(swapFees.ConfirmationsNeeded)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fee := a.feeCalculator.Fee(outputAmount, feeRate, false)
|
|
total := outputAmount + fee
|
|
totalForUser := total - collectAmount
|
|
|
|
if total > a.utxoBalance() || totalForUser > a.totalBalance() {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
AmountInSat: payment.AmountInSat,
|
|
FeeInSat: fee,
|
|
TotalInSat: totalForUser,
|
|
SwapFees: swapFees,
|
|
}, nil
|
|
}
|
|
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusOk,
|
|
AmountInSat: payment.AmountInSat,
|
|
FeeInSat: fee,
|
|
TotalInSat: totalForUser,
|
|
SwapFees: swapFees,
|
|
}, nil
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) analyzeTFFAAmountlessInvoiceSwap(payment *PaymentToInvoice) (*PaymentAnalysis, error) {
|
|
zeroConfFeeRate, err := a.feeWindow.SwapFeeRate(0)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
zeroConfFeeInSat := a.computeFeeForTFFASwap(payment, zeroConfFeeRate)
|
|
if zeroConfFeeInSat > a.totalBalance() {
|
|
// We can't even pay the onchain fee
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
FeeInSat: zeroConfFeeInSat,
|
|
TotalInSat: a.totalBalance() + int64(zeroConfFeeRate),
|
|
}, nil
|
|
}
|
|
|
|
params, err := a.computeParamsForTFFASwap(payment, 0)
|
|
if err != nil {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
FeeInSat: zeroConfFeeInSat,
|
|
TotalInSat: a.totalBalance() + int64(zeroConfFeeInSat),
|
|
}, nil
|
|
}
|
|
|
|
confirmations := payment.FundingOutputPolicies.FundingConfirmations(params.Amount, params.RoutingFee)
|
|
if confirmations == 1 {
|
|
params, err = a.computeParamsForTFFASwap(payment, 1)
|
|
if err != nil {
|
|
// This LITERALLY can never happen, as only source of error for computeParamsForTFFASwap are:
|
|
// - negative conf target (we're using 1)
|
|
// - no route for amount (would be catched by previous call since amount with 1-conf fee would be smaller)
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
FeeInSat: zeroConfFeeInSat,
|
|
TotalInSat: a.totalBalance() + int64(zeroConfFeeInSat),
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
amount := params.Amount
|
|
lightningFee := params.RoutingFee
|
|
onChainFee := params.OnChainFee
|
|
|
|
if amount <= 0 {
|
|
// We can't pay the combined fee
|
|
// This can be either cause we can't pay both fees summed or we had to bump to
|
|
// 1-conf and we can't pay that.
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
FeeInSat: zeroConfFeeInSat,
|
|
TotalInSat: a.totalBalance() + int64(zeroConfFeeInSat),
|
|
}, nil
|
|
}
|
|
|
|
swapFees := fees.ComputeSwapFees(amount, payment.BestRouteFees, payment.FundingOutputPolicies, true)
|
|
|
|
if swapFees.DebtType == fees.DebtTypeLend {
|
|
return nil, errors.New("TFFA swap should not be a lend operation")
|
|
}
|
|
|
|
// This assumes that debt amount is either 0 (DebtType = NONE) or positive (DebtType = COLLECT)
|
|
outputAmount := amount + lightningFee + swapFees.OutputPadding + swapFees.DebtAmount
|
|
|
|
if lightningFee != swapFees.RoutingFee {
|
|
return nil, fmt.Errorf(
|
|
"integrity error: inconsistent lightning fee calculated for TFFA swap (lightning_fee=%v, output_amount=%v, original_amount=%v, routing_fee=%v, output_padding=%v)",
|
|
int64(lightningFee),
|
|
int64(outputAmount),
|
|
payment.AmountInSat,
|
|
int64(swapFees.RoutingFee),
|
|
int64(swapFees.OutputPadding),
|
|
)
|
|
}
|
|
|
|
total := outputAmount + onChainFee
|
|
totalForDisplay := total - swapFees.DebtAmount // amount + lightningFee + outputPadding + onChainFee
|
|
|
|
// We need to ensure we can spend on chain and that we have enough UI visible balance too
|
|
// That is, the collect doesn't make us spend more than we really can and the amount + fee
|
|
// doesn't default any debt.
|
|
canPay := total <= btcutil.Amount(a.utxoBalance()) && totalForDisplay <= btcutil.Amount(a.totalBalance())
|
|
|
|
if !canPay {
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusUnpayable,
|
|
AmountInSat: int64(amount),
|
|
FeeInSat: int64(onChainFee),
|
|
TotalInSat: payment.AmountInSat,
|
|
SwapFees: swapFees,
|
|
}, nil
|
|
}
|
|
|
|
return &PaymentAnalysis{
|
|
Status: AnalysisStatusOk,
|
|
AmountInSat: int64(amount),
|
|
FeeInSat: int64(onChainFee),
|
|
TotalInSat: payment.AmountInSat,
|
|
SwapFees: swapFees,
|
|
}, nil
|
|
}
|
|
|
|
type swapParams struct {
|
|
Amount btcutil.Amount
|
|
OnChainFee btcutil.Amount
|
|
RoutingFee btcutil.Amount
|
|
}
|
|
|
|
// computeParamsForTFFASwap takes care of the VERY COMPLEX tasks of calculating
|
|
// the amount, routing fee and on-chain fee for a TFFA swap. Calculating the
|
|
// on-chain fee is pretty straightforward but for the other two we use what
|
|
// we call "the equation". Let's dive into how it works:
|
|
//
|
|
// Let:
|
|
// - x be the payment amount
|
|
// - l be the lightning/routing fee
|
|
// - h be the onchain fee for amount x
|
|
// - y the available user balance (utxoBalance - expectedDebt)
|
|
//
|
|
// Also, given a specific route, l is a linear function of x l(x) = a * x + b,
|
|
// where a and b are known.
|
|
//
|
|
// For amountLessInvoices, we need to figure out x and l such that x + l = y - h
|
|
//
|
|
// Note:
|
|
// - we don't care about debt (except to calculate user balance) since it
|
|
// doesn't affect that payment/offchain amount and thus the routingFee. It may
|
|
// affect the onchain fee though, which is already calculated considering it.
|
|
// - we don't care about output padding, since it can either be:
|
|
// - issued debt, in which case point above still holds
|
|
// - taken by fee, which would mean we are in a TFFA for a sub-dust amount,
|
|
// which is unpayable since we don't have balance to add as padding/fee.
|
|
//
|
|
// Suppose we have only one route.
|
|
// Then, x + l(x) = y - h
|
|
// Then, x + (a * x + b) = y - h
|
|
// Then, x * (1 + a) = y - h - b
|
|
// Then, x = (y - h - b) / (a + 1) (*1)
|
|
//
|
|
// BUT, we can have different routes for different amounts, aka:
|
|
// l_1(x) = a_1 * x + b_1
|
|
// l_2(x) = a_2 * x + b_2
|
|
// l_3(x) = a_3 * x + b_3
|
|
// etc...
|
|
//
|
|
// What we can do is try out each l(x) corresponding to each route and check
|
|
// which one gives us a valid amount (e.g amount < route.maxCapacityInSat), in
|
|
// other words, which route is the "best route" associated with that amount.
|
|
// BestRouteFees guarantees that for each amount there's only one "best route"
|
|
// and that there is a route for each amount.
|
|
//
|
|
// Final note, our final equation looks a little different since our param a
|
|
// is given as FeeProportionalMillionth/1_000_000, so in order to not lose
|
|
// precision we need to massage equation *1 bit more:
|
|
//
|
|
// x = (y - h - b) / (FeeProportionalMillionth/1_000_000 + 1)
|
|
// x = (y - h - b) / (FeeProportionalMillionth + 1_000_000) / 1_000_000
|
|
// x = ((y - h - b) * 1_000_000) / (FeeProportionalMillionth + 1_000_000)
|
|
//
|
|
func (a *PaymentAnalyzer) computeParamsForTFFASwap(payment *PaymentToInvoice, confs uint) (*swapParams, error) {
|
|
feeRate, err := a.feeWindow.SwapFeeRate(confs)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
onChainFeeInSat := a.computeFeeForTFFASwap(payment, feeRate)
|
|
|
|
for _, bestRouteFees := range payment.BestRouteFees {
|
|
amount := btcutil.Amount(
|
|
(a.totalBalance() - onChainFeeInSat - int64(bestRouteFees.FeeBase)) * 1_000_000 /
|
|
(int64(bestRouteFees.FeeProportionalMillionth) + 1_000_000))
|
|
|
|
lightningFee := bestRouteFees.ForAmount(amount)
|
|
|
|
// Warning, this is an ugly hack because we are not (yet) using millisats as unit of
|
|
// account thus we might an amount 1 sat smaller than the previous implementation.
|
|
// What this check does is to see if we can send just one sat more with the safe fee
|
|
// in sats. It probably has some edge cases in some cases but we have been ignoring
|
|
// them for now so we can live with it.
|
|
if bestRouteFees.ForAmount(amount+1) == lightningFee {
|
|
amount += 1
|
|
}
|
|
|
|
if amount+lightningFee <= bestRouteFees.MaxCapacity {
|
|
|
|
// There's a special comment to be made here for VERY edgy case where
|
|
// bestRouteFees.ForAmount(amount+1) == lightningFee+1.
|
|
// In this case adding 1 sat to the amount makes you need an extra sat for the
|
|
// routingFee, and these 2 extra sats make the total go over userBalance, but
|
|
// there's 1 sat available in our balance. What do we do it? Answer: nothing,
|
|
// it will be burn as on-chain fee.
|
|
|
|
return &swapParams{
|
|
Amount: amount,
|
|
RoutingFee: lightningFee,
|
|
OnChainFee: btcutil.Amount(onChainFeeInSat),
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, errors.New("none of the best route fees have enough capacity")
|
|
}
|
|
|
|
func (a *PaymentAnalyzer) computeFeeForTFFASwap(payment *PaymentToInvoice, feeRate float64) int64 {
|
|
// Compute tha on-chain fee. As its TFFA, we want to calculate the fee for the total balance
|
|
// including any sats we want to collect.
|
|
onChainAmount := a.totalBalance() + int64(payment.FundingOutputPolicies.PotentialCollect)
|
|
|
|
return a.feeCalculator.Fee(onChainAmount, feeRate, true)
|
|
}
|
|
|
|
// MaxFeeRateToAddress computes the maximum fee rate that can be used when
|
|
// paying a given amount. This does not imply that the payment _can be made_.
|
|
// When given invalid parameters, it's likely to still obtain a value here and
|
|
// the resulting analysis would be Unpayable. It's up to the caller to first
|
|
// verify the amount is payable, and only then call this method.
|
|
func (a *PaymentAnalyzer) MaxFeeRateToAddress(payment *PaymentToAddress) float64 {
|
|
|
|
if payment.AmountInSat > a.totalBalance() {
|
|
return 0
|
|
}
|
|
|
|
var restInSat int64
|
|
if payment.TakeFeeFromAmount {
|
|
restInSat = payment.AmountInSat
|
|
} else {
|
|
restInSat = a.totalBalance() - payment.AmountInSat
|
|
}
|
|
|
|
for _, sizeForAmount := range a.nextTransactionSize.SizeProgression {
|
|
if sizeForAmount.AmountInSat >= payment.AmountInSat {
|
|
return float64(restInSat) / float64(sizeForAmount.SizeInVByte)
|
|
}
|
|
}
|
|
|
|
return 0
|
|
}
|