Files
muun-recovery/libwallet/operation/payment_analyzer_test.go
2025-05-13 17:49:44 -03:00

2409 lines
57 KiB
Go

package operation
import (
"math"
"reflect"
"testing"
"github.com/muun/libwallet/fees"
)
var defaultFeeWindow = &FeeWindow{
TargetedFees: func() map[uint]float64 {
fees := make(map[uint]float64)
fees[1] = 10.0
fees[2] = 5.0
fees[3] = 1.0
return fees
}(),
}
var feeWindowFailureTests = &FeeWindow{
TargetedFees: func() map[uint]float64 {
fees := make(map[uint]float64)
fees[1] = 100.0
fees[5] = 50.0
fees[10] = .25
return fees
}(),
}
var defaultNTS = &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 1_000_000,
SizeInVByte: 100,
},
},
ExpectedDebtInSat: 0,
}
func TestAnalyzeOnChain(t *testing.T) {
testCases := []struct {
desc string
nts *NextTransactionSize
payment *PaymentToAddress
expected *PaymentAnalysis
err bool
}{
{
desc: "success",
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 10000,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 10000,
FeeInSat: 100,
TotalInSat: 10100,
},
},
{
desc: "take fee from amount",
payment: &PaymentToAddress{
AmountInSat: 1_000_000,
TakeFeeFromAmount: true,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 999_900,
FeeInSat: 100,
TotalInSat: 1_000_000,
},
},
{
desc: "zero amount",
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 0,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountTooSmall,
},
},
{
desc: "zero amount using TFFA",
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 0,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountTooSmall,
},
},
{
desc: "sub dust amount",
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 100,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountTooSmall,
},
},
{
desc: "sub dust amount using TFFA",
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 100,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountTooSmall,
},
},
{
desc: "amount greater than balance",
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 1_000_000_000,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountGreaterThanBalance,
TotalInSat: 1_000_000_000,
},
},
{
desc: "valid amount plus selected fee",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 5000,
FeeRateInSatsPerVByte: 10,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 5000,
FeeInSat: 2400,
TotalInSat: 7400,
},
},
{
desc: "valid amount but unpayable because of fee",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 9000,
FeeRateInSatsPerVByte: 10,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 9000,
FeeInSat: 2400,
TotalInSat: 11400,
},
},
{
desc: "valid amount but unpayable with any fee",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 9999,
FeeRateInSatsPerVByte: 10,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 9999,
FeeInSat: 2400,
TotalInSat: 12399,
},
},
{
desc: "valid amount plus fee using TFFA",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 10_000,
FeeRateInSatsPerVByte: 10,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 7600,
FeeInSat: 2400,
TotalInSat: 10_000,
},
},
{
desc: "invalid amount using TFFA",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 9900,
FeeRateInSatsPerVByte: 10,
},
err: true,
},
{
desc: "valid amount but unpayable because of fee using TFFA",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 1_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 1000,
FeeRateInSatsPerVByte: 10,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 0,
FeeInSat: 2400,
TotalInSat: 1000,
},
},
{
desc: "valid amount but unpayable with any fee using TFFA",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 600,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 600,
FeeRateInSatsPerVByte: 10,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 0,
FeeInSat: 2400,
TotalInSat: 600,
},
},
{
desc: "success with debt > 0",
nts: &NextTransactionSize{
SizeProgression: defaultNTS.SizeProgression,
ExpectedDebtInSat: 10000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 10000,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 10000,
FeeInSat: 100,
TotalInSat: 10100,
},
},
{
desc: "take fee from amount success with debt > 0",
nts: &NextTransactionSize{
SizeProgression: defaultNTS.SizeProgression,
ExpectedDebtInSat: 10000,
},
payment: &PaymentToAddress{
AmountInSat: 990_000,
TakeFeeFromAmount: true,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 989_900,
FeeInSat: 100,
TotalInSat: 990_000,
},
},
{
desc: "amount greater than balance because debt > 0",
nts: &NextTransactionSize{
SizeProgression: defaultNTS.SizeProgression,
ExpectedDebtInSat: 10000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 999_900,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountGreaterThanBalance,
TotalInSat: 999_900,
},
},
{
desc: "unpayable because debt > 0",
nts: &NextTransactionSize{
SizeProgression: defaultNTS.SizeProgression,
ExpectedDebtInSat: 10000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 989_500,
FeeRateInSatsPerVByte: 10,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 989_500,
FeeInSat: 1000,
TotalInSat: 990_500,
},
},
{
desc: "amount greater than balance using TFFA because debt > 0",
nts: &NextTransactionSize{
SizeProgression: defaultNTS.SizeProgression,
ExpectedDebtInSat: 10000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 999_900,
FeeRateInSatsPerVByte: 1,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountGreaterThanBalance,
TotalInSat: 999_900,
},
},
{
desc: "unpayable using TFFA because debt > 0",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 8000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 2000,
FeeRateInSatsPerVByte: 100,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 0,
FeeInSat: 24000,
TotalInSat: 2000,
},
},
{
desc: "unpayable using TFFA because amount < DUST",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 7400,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 2600,
FeeRateInSatsPerVByte: 10,
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 200,
FeeInSat: 2400,
TotalInSat: 2600,
},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
var analyzer *PaymentAnalyzer
if tC.nts != nil {
analyzer = NewPaymentAnalyzer(defaultFeeWindow, tC.nts)
} else {
analyzer = NewPaymentAnalyzer(defaultFeeWindow, defaultNTS)
}
analysis, err := analyzer.ToAddress(tC.payment)
if err == nil && tC.err {
t.Fatal("expected analysis to error")
}
if err != nil && !tC.err {
t.Fatal(err)
}
if !reflect.DeepEqual(analysis, tC.expected) {
t.Fatalf("analysis does not match expected, got %+v, expected %+v", analysis, tC.expected)
}
})
}
}
func TestAnalyzeOnChainValidAmountButUnpayableWithAnyFee(t *testing.T) {
analyzer := NewPaymentAnalyzer(defaultFeeWindow, &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
})
analysis, err := analyzer.ToAddress(&PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 9999,
FeeRateInSatsPerVByte: 10,
})
if err != nil {
t.Fatal(err)
}
if analysis.Status != AnalysisStatusUnpayable {
t.Fatal("expected analysis to be unpayable")
}
if analysis.FeeInSat != 2400 {
t.Fatalf("expected fee to be %v, but got %v", 2400, analysis.FeeInSat)
}
if analysis.TotalInSat != 12399 {
t.Fatalf("expected total to be %v, but got %v", 12399, analysis.TotalInSat)
}
analysis, err = analyzer.ToAddress(&PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 9999,
FeeRateInSatsPerVByte: 0.25,
})
if err != nil {
t.Fatal(err)
}
if analysis.Status != AnalysisStatusUnpayable {
t.Fatal("expected analysis to be unpayable")
}
if analysis.FeeInSat != 60 {
t.Fatalf("expected fee to be %v, but got %v", 60, analysis.FeeInSat)
}
if analysis.TotalInSat != 10059 {
t.Fatalf("expected total to be %v, but got %v", 10059, analysis.TotalInSat)
}
}
func TestAnalyzeOnChainValidAmountButUnpayableWithAnyFeeUsingTFFA(t *testing.T) {
analyzer := NewPaymentAnalyzer(defaultFeeWindow, &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 600,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
})
analysis, err := analyzer.ToAddress(&PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 600,
FeeRateInSatsPerVByte: 10,
})
if err != nil {
t.Fatal(err)
}
if analysis.Status != AnalysisStatusUnpayable {
t.Fatal("expected analysis to be unpayable")
}
if analysis.FeeInSat != 2400 {
t.Fatalf("expected fee to be %v, but got %v", 2400, analysis.FeeInSat)
}
if analysis.TotalInSat != 600 {
t.Fatalf("expected total to be %v, but got %v", 600, analysis.TotalInSat)
}
analysis, err = analyzer.ToAddress(&PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 600,
FeeRateInSatsPerVByte: 0.25,
})
if err != nil {
t.Fatal(err)
}
if analysis.Status != AnalysisStatusUnpayable {
t.Fatal("expected analysis to be unpayable")
}
if analysis.FeeInSat != 60 {
t.Fatalf("expected fee to be %v, but got %v", 60, analysis.FeeInSat)
}
if analysis.AmountInSat != 540 {
t.Fatalf("expected amount to be %v, but got %v", 540, analysis.TotalInSat)
}
if analysis.TotalInSat != 600 {
t.Fatalf("expected total to be %v, but got %v", 600, analysis.TotalInSat)
}
}
func TestAnalyzeOffChain(t *testing.T) {
testCases := []struct {
desc string
feeWindow *FeeWindow
nts *NextTransactionSize
payment *PaymentToInvoice
expected *PaymentAnalysis
err bool
}{
{
desc: "swap with amount too small (zero funds)",
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 0,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountTooSmall,
},
},
{
desc: "swap with amount too small (negative funds)",
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: -10,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountTooSmall,
},
},
{
desc: "swap with amount greater than balance",
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 5_000_000,
SwapFees: &fees.SwapFees{
OutputAmount: 5_000_000,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountGreaterThanBalance,
TotalInSat: 5_000_000,
},
},
{
desc: "swap with integrity error",
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 500_000,
SwapFees: &fees.SwapFees{
OutputAmount: 500_001,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
err: true,
},
{
desc: "swap COLLECT with integrity error",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 5_000_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 1000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 500_000,
SwapFees: &fees.SwapFees{
OutputAmount: 501_001,
DebtType: fees.DebtTypeCollect,
DebtAmount: 1000,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
err: true,
},
{
desc: "swap with valid amount with no routing fee",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 100,
SwapFees: &fees.SwapFees{
OutputAmount: 1100,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 0,
OutputPadding: 1000,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 100,
FeeInSat: 240,
TotalInSat: 1340,
SwapFees: &fees.SwapFees{
OutputAmount: 1100,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 0,
OutputPadding: 1000,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap with valid amount",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 100,
SwapFees: &fees.SwapFees{
OutputAmount: 1101,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 1,
OutputPadding: 1000,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 100,
FeeInSat: 240,
TotalInSat: 1341,
SwapFees: &fees.SwapFees{
OutputAmount: 1101,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 1,
OutputPadding: 1000,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap with valid amount with 1-conf",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 100,
SwapFees: &fees.SwapFees{
OutputAmount: 1101,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 1,
OutputPadding: 1000,
ConfirmationsNeeded: 1,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 100,
FeeInSat: 2400,
TotalInSat: 3501,
SwapFees: &fees.SwapFees{
OutputAmount: 1101,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 1,
OutputPadding: 1000,
ConfirmationsNeeded: 1,
},
},
},
{
desc: "swap with valid FIXED amount using TFFA",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 10_000,
SwapFees: &fees.SwapFees{
OutputAmount: 10_000,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
err: true,
},
{
desc: "swap valid amount but unpayable because of fee",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 500,
SwapFees: &fees.SwapFees{
OutputAmount: 10000,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 1000,
OutputPadding: 8500,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 500,
FeeInSat: 240,
TotalInSat: 10240,
SwapFees: &fees.SwapFees{
OutputAmount: 10000,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 1000,
OutputPadding: 8500,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap valid amount but unpayable because of output padding",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 500,
SwapFees: &fees.SwapFees{
OutputAmount: 11000,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 1000,
OutputPadding: 9500,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 500,
FeeInSat: 240,
TotalInSat: 11240,
SwapFees: &fees.SwapFees{
OutputAmount: 11000,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 1000,
OutputPadding: 9500,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap LEND success",
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 100,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeLend,
DebtAmount: 100,
RoutingFee: 1,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 100,
FeeInSat: 0,
TotalInSat: 101,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeLend,
DebtAmount: 100,
RoutingFee: 1,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap LEND success with no routing fee",
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 100,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeLend,
DebtAmount: 100,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 100,
FeeInSat: 0,
TotalInSat: 100,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeLend,
DebtAmount: 100,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap LEND amount greater than balance",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 1_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 900,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 200,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeLend,
DebtAmount: 200,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusAmountGreaterThanBalance,
TotalInSat: 200,
},
},
{
desc: "swap LEND unpayable",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 1_000,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 900,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 100,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeLend,
DebtAmount: 100,
RoutingFee: 10,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 100,
FeeInSat: 0,
TotalInSat: 110,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeLend,
DebtAmount: 100,
RoutingFee: 10,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap full COLLECT success with no routing fee",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 5000,
SwapFees: &fees.SwapFees{
OutputAmount: 8000,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 5000,
FeeInSat: 240,
TotalInSat: 5240,
SwapFees: &fees.SwapFees{
OutputAmount: 8000,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap full COLLECT success",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 5000,
SwapFees: &fees.SwapFees{
OutputAmount: 8001,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 1,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 5000,
FeeInSat: 240,
TotalInSat: 5241,
SwapFees: &fees.SwapFees{
OutputAmount: 8001,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 1,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap full COLLECT success with 1-conf",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 5000,
SwapFees: &fees.SwapFees{
OutputAmount: 8001,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 1,
OutputPadding: 0,
ConfirmationsNeeded: 1,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 5000,
FeeInSat: 2400,
TotalInSat: 7401,
SwapFees: &fees.SwapFees{
OutputAmount: 8001,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 1,
OutputPadding: 0,
ConfirmationsNeeded: 1,
},
},
},
{
desc: "swap COLLECT output amount greater than balance (aka unpayable)",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 7400,
SwapFees: &fees.SwapFees{
OutputAmount: 10500,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 100,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 7400,
FeeInSat: 240,
TotalInSat: 7740,
SwapFees: &fees.SwapFees{
OutputAmount: 10500,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 100,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap COLLECT amount plus fee greater than balance (aka unpayable)",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10500,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 7400,
SwapFees: &fees.SwapFees{
OutputAmount: 10500,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 100,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 7400,
FeeInSat: 240,
TotalInSat: 7740,
SwapFees: &fees.SwapFees{
OutputAmount: 10500,
DebtType: fees.DebtTypeCollect,
DebtAmount: 3000,
RoutingFee: 100,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap partial COLLECT success with no routing fee",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 20401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 8000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 12160,
SwapFees: &fees.SwapFees{
OutputAmount: 14160,
DebtType: fees.DebtTypeCollect,
DebtAmount: 2000,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 12160,
FeeInSat: 240,
TotalInSat: 12400,
SwapFees: &fees.SwapFees{
OutputAmount: 14160,
DebtType: fees.DebtTypeCollect,
DebtAmount: 2000,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap partial COLLECT success",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 20401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 8000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 12160,
SwapFees: &fees.SwapFees{
OutputAmount: 14161,
DebtType: fees.DebtTypeCollect,
DebtAmount: 2000,
RoutingFee: 1,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 12160,
FeeInSat: 240,
TotalInSat: 12401,
SwapFees: &fees.SwapFees{
OutputAmount: 14161,
DebtType: fees.DebtTypeCollect,
DebtAmount: 2000,
RoutingFee: 1,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap partial COLLECT for unpayable amount",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 20401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 8000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 12401,
SwapFees: &fees.SwapFees{
OutputAmount: 14401,
DebtType: fees.DebtTypeCollect,
DebtAmount: 2000,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 12401,
FeeInSat: 240,
TotalInSat: 12641,
SwapFees: &fees.SwapFees{
OutputAmount: 14401,
DebtType: fees.DebtTypeCollect,
DebtAmount: 2000,
RoutingFee: 0,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap amountless invoice as LEND swap success",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 20401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 12401,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 1000000,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 12401,
FeeInSat: 0,
TotalInSat: 12411,
SwapFees: &fees.SwapFees{
OutputAmount: 0,
DebtType: fees.DebtTypeLend,
DebtAmount: 12411,
RoutingFee: 10,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap amountless invoice as COLLECT swap success",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 20401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 12401,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 100,
PotentialCollect: 1010,
MaxAmountFor0Conf: 1000000,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 12401,
FeeInSat: 240,
TotalInSat: 12651,
SwapFees: &fees.SwapFees{
OutputAmount: 13421,
DebtType: fees.DebtTypeCollect,
DebtAmount: 1010,
RoutingFee: 10,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap amountless invoice as NON DEBT swap success",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 20401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 12401,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 1000000,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 12401,
FeeInSat: 240,
TotalInSat: 12651,
SwapFees: &fees.SwapFees{
OutputAmount: 12411,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 10,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap amountless invoice one-conf",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 20401,
SizeInVByte: 240,
},
},
ExpectedDebtInSat: 3000,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: false,
AmountInSat: 12401,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1,
FeeBase: 10,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 12401,
FeeInSat: 2400,
TotalInSat: 14811,
SwapFees: &fees.SwapFees{
OutputAmount: 12411,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
RoutingFee: 10,
OutputPadding: 0,
ConfirmationsNeeded: 1,
},
},
},
{
desc: "swap amountless invoice TFFA one-conf",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 369_310,
SizeInVByte: 253,
},
},
ExpectedDebtInSat: 26876,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 342434,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 1000000,
FeeProportionalMillionth: 1051,
FeeBase: 4,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 123124,
PotentialCollect: 20686,
MaxAmountFor0Conf: 0,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 316798,
FeeInSat: 25300,
TotalInSat: 342434,
SwapFees: &fees.SwapFees{
OutputAmount: 337820,
DebtType: fees.DebtTypeCollect,
DebtAmount: 20686,
RoutingFee: 336,
OutputPadding: 0,
ConfirmationsNeeded: 1,
},
},
},
{
desc: "swap amountless invoice TFFA with invalid amount",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 36_931,
SizeInVByte: 253,
},
},
ExpectedDebtInSat: 26876,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 10054,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1051,
FeeBase: 4,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 123124,
PotentialCollect: 20686,
MaxAmountFor0Conf: 193437,
},
},
err: true,
},
{
desc: "swap amountless invoice TFFA cant pay combined fee",
feeWindow: &FeeWindow{
TargetedFees: func() map[uint]float64 {
fees := make(map[uint]float64)
fees[1] = 100.0
fees[5] = 50.0
fees[10] = 3.1
return fees
}(),
},
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 800,
SizeInVByte: 253,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 800,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1051,
FeeBase: 40,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 123124,
PotentialCollect: 20686,
MaxAmountFor0Conf: 193437,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 0,
FeeInSat: 785,
TotalInSat: 1585,
},
},
{
desc: "swap amountless invoice TFFA (failure 1)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 36_931,
SizeInVByte: 253,
},
},
ExpectedDebtInSat: 26876,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 10055,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1051,
FeeBase: 4,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 123124,
PotentialCollect: 20686,
MaxAmountFor0Conf: 193437,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 9977,
FeeInSat: 64,
TotalInSat: 10055,
SwapFees: &fees.SwapFees{
OutputAmount: 30677,
DebtType: fees.DebtTypeCollect,
DebtAmount: 20686,
RoutingFee: 14,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 2)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 83880,
SizeInVByte: 495,
},
},
ExpectedDebtInSat: 73590,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 10290,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 297,
FeeBase: 4,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 76410,
PotentialCollect: 33292,
MaxAmountFor0Conf: 254235,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 10159,
FeeInSat: 124,
TotalInSat: 10290,
SwapFees: &fees.SwapFees{
OutputAmount: 43458,
DebtType: fees.DebtTypeCollect,
DebtAmount: 33292,
RoutingFee: 7,
OutputPadding: 0,
ConfirmationsNeeded: 0,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 3)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 1597,
SizeInVByte: 391,
},
},
ExpectedDebtInSat: 1511,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 86,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 875,
FeeBase: 2,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 148489,
PotentialCollect: 759,
MaxAmountFor0Conf: 163704,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
FeeInSat: 98,
TotalInSat: 86,
},
},
{
desc: "swap amountless invoice TFFA (failure 4)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 1016,
SizeInVByte: 409,
},
},
ExpectedDebtInSat: 776,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 240,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 468,
FeeBase: 4,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 149224,
PotentialCollect: 246,
MaxAmountFor0Conf: 243320,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 133,
FeeInSat: 103,
TotalInSat: 240,
SwapFees: &fees.SwapFees{
RoutingFee: 4,
OutputPadding: 163,
DebtType: fees.DebtTypeCollect,
DebtAmount: 246,
OutputAmount: 546,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 5)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 1843,
SizeInVByte: 475,
},
},
ExpectedDebtInSat: 458,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 1385,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1417,
FeeBase: 2,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 149542,
PotentialCollect: 222,
MaxAmountFor0Conf: 223421,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 1263,
FeeInSat: 119,
TotalInSat: 1385,
SwapFees: &fees.SwapFees{
RoutingFee: 3,
OutputPadding: 0,
DebtType: fees.DebtTypeCollect,
DebtAmount: 222,
OutputAmount: 1488,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 6)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 6658,
SizeInVByte: 262,
},
},
ExpectedDebtInSat: 6539,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 119,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1991,
FeeBase: 1,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 143461,
PotentialCollect: 6422,
MaxAmountFor0Conf: 254856,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 52,
FeeInSat: 66,
TotalInSat: 119,
SwapFees: &fees.SwapFees{
RoutingFee: 1,
OutputPadding: 0,
DebtType: fees.DebtTypeCollect,
DebtAmount: 6422,
OutputAmount: 6475,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 7)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 8169,
SizeInVByte: 301,
},
},
ExpectedDebtInSat: 5619,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 2550,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1009,
FeeBase: 2,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 144381,
PotentialCollect: 1140,
MaxAmountFor0Conf: 210617,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 2470,
FeeInSat: 76,
TotalInSat: 2550,
SwapFees: &fees.SwapFees{
RoutingFee: 4,
OutputPadding: 0,
DebtType: fees.DebtTypeCollect,
DebtAmount: 1140,
OutputAmount: 3614,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 8)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 46785,
SizeInVByte: 365,
},
{
AmountInSat: 99948,
SizeInVByte: 857,
},
},
ExpectedDebtInSat: 40471,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 59477,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 655,
FeeBase: 1,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 109529,
PotentialCollect: 13892,
MaxAmountFor0Conf: 258399,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 59223,
FeeInSat: 215,
TotalInSat: 59477,
SwapFees: &fees.SwapFees{
RoutingFee: 39,
OutputPadding: 0,
DebtType: fees.DebtTypeCollect,
DebtAmount: 13892,
OutputAmount: 73154,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 9)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10087,
SizeInVByte: 430,
},
{
AmountInSat: 156784,
SizeInVByte: 851,
},
},
ExpectedDebtInSat: 84117,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 72667,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1146,
FeeBase: 3,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 65883,
PotentialCollect: 77393,
MaxAmountFor0Conf: 240440,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 72369,
FeeInSat: 213,
TotalInSat: 72667,
SwapFees: &fees.SwapFees{
RoutingFee: 85,
OutputPadding: 0,
DebtType: fees.DebtTypeCollect,
DebtAmount: 77393,
OutputAmount: 149847,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 10)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 117197,
SizeInVByte: 333,
},
},
ExpectedDebtInSat: 18225,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 98972,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 890,
FeeBase: 1,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 131775,
PotentialCollect: 16109,
MaxAmountFor0Conf: 182743,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 98800,
FeeInSat: 84,
TotalInSat: 98972,
SwapFees: &fees.SwapFees{
RoutingFee: 88,
OutputPadding: 0,
DebtType: fees.DebtTypeCollect,
DebtAmount: 16109,
OutputAmount: 114997,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 11)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 3165,
SizeInVByte: 402,
},
},
ExpectedDebtInSat: 2812,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 353,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1461,
FeeBase: 3,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 147188,
PotentialCollect: 222,
MaxAmountFor0Conf: 164563,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 249,
FeeInSat: 101,
TotalInSat: 353,
SwapFees: &fees.SwapFees{
RoutingFee: 3,
OutputPadding: 72,
DebtType: fees.DebtTypeCollect,
DebtAmount: 222,
OutputAmount: 546,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 11 bis)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 3165,
SizeInVByte: 402,
},
},
ExpectedDebtInSat: 2740,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 425,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1461,
FeeBase: 3,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 147188,
PotentialCollect: 222,
MaxAmountFor0Conf: 164563,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 321,
FeeInSat: 101,
TotalInSat: 425,
SwapFees: &fees.SwapFees{
RoutingFee: 3,
OutputPadding: 0,
DebtType: fees.DebtTypeCollect,
DebtAmount: 222,
OutputAmount: 546,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 12)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 86343,
SizeInVByte: 358,
},
},
ExpectedDebtInSat: 32639,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 53704,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1457,
FeeBase: 2,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 117361,
PotentialCollect: 27161,
MaxAmountFor0Conf: 287685,
},
},
// In this scenario there's 1 sat that is lost and will be burn in fees to the miner
expected: &PaymentAnalysis{
Status: AnalysisStatusOk,
AmountInSat: 53534,
FeeInSat: 90,
TotalInSat: 53704,
SwapFees: &fees.SwapFees{
RoutingFee: 79,
OutputPadding: 0,
DebtType: fees.DebtTypeCollect,
DebtAmount: 27161,
OutputAmount: 80774,
},
},
},
{
desc: "swap amountless invoice TFFA (failure 13)",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 500,
SizeInVByte: 209,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 500,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 100000,
FeeProportionalMillionth: 1000,
FeeBase: 1,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 0,
PotentialCollect: 0,
MaxAmountFor0Conf: math.MaxInt64,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
AmountInSat: 446,
FeeInSat: 53,
TotalInSat: 500,
SwapFees: &fees.SwapFees{
RoutingFee: 1,
OutputPadding: 99,
DebtType: fees.DebtTypeNone,
DebtAmount: 0,
OutputAmount: 546,
},
},
},
{
desc: "swap amountless invoice TFFA with no best route for amount",
feeWindow: feeWindowFailureTests,
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 117197,
SizeInVByte: 333,
},
},
ExpectedDebtInSat: 18225,
},
payment: &PaymentToInvoice{
TakeFeeFromAmount: true,
AmountInSat: 98972,
BestRouteFees: []fees.BestRouteFees{
{
MaxCapacity: 90000,
FeeProportionalMillionth: 890,
FeeBase: 1,
},
},
FundingOutputPolicies: &fees.FundingOutputPolicies{
MaximumDebt: 131775,
PotentialCollect: 16109,
MaxAmountFor0Conf: 182743,
},
},
expected: &PaymentAnalysis{
Status: AnalysisStatusUnpayable,
FeeInSat: 84,
TotalInSat: 99056,
},
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
nts := defaultNTS
if tC.nts != nil {
nts = tC.nts
}
feeWindow := defaultFeeWindow
if tC.feeWindow != nil {
feeWindow = tC.feeWindow
}
analyzer := NewPaymentAnalyzer(feeWindow, nts)
analysis, err := analyzer.ToInvoice(tC.payment)
if err == nil && tC.err {
t.Fatal("expected analysis to error")
}
if err != nil && !tC.err {
t.Fatal(err)
}
if !reflect.DeepEqual(analysis, tC.expected) {
t.Fatalf(
"analysis does not match expected\n analysis: got %+v, expected %+v\nswapfees: got %+v, expected %+v",
analysis,
tC.expected,
analysis.SwapFees,
tC.expected.SwapFees,
)
}
})
}
}
func TestMaxFeeRate(t *testing.T) {
testCases := []struct {
desc string
nts *NextTransactionSize
payment *PaymentToAddress
expected float64
}{
{
desc: "small amount with one coin",
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 10_000,
},
expected: 9_900,
},
{
desc: "take fee from amount one coin",
payment: &PaymentToAddress{
AmountInSat: 1_000_000,
TakeFeeFromAmount: true,
},
expected: 10_000,
},
{
desc: "zero amount",
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 0,
},
expected: 10_000,
},
{
desc: "zero amount using TFFA",
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 0,
},
expected: 0,
},
{
desc: "amount greater than balance",
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 1_000_000_000,
},
expected: 0,
},
{
desc: "small amount with one coin and debt > 0",
nts: &NextTransactionSize{
SizeProgression: defaultNTS.SizeProgression,
ExpectedDebtInSat: 10_000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 10_000,
},
expected: 9_800,
},
{
desc: "take fee from amount success with debt > 0",
nts: &NextTransactionSize{
SizeProgression: defaultNTS.SizeProgression,
ExpectedDebtInSat: 10_000,
},
payment: &PaymentToAddress{
AmountInSat: 990_000,
TakeFeeFromAmount: true,
},
expected: 9_900,
},
{
desc: "amount greater than balance because debt > 0",
nts: &NextTransactionSize{
SizeProgression: defaultNTS.SizeProgression,
ExpectedDebtInSat: 10_000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 999_900,
},
expected: 0,
},
{
desc: "needs 2 coins to spend",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
{
AmountInSat: 20_000,
SizeInVByte: 450,
},
},
ExpectedDebtInSat: 0,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 11_000,
},
expected: 20,
},
{
desc: "needs 2 coins to spend with debt",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
{
AmountInSat: 20_000,
SizeInVByte: 400,
},
},
ExpectedDebtInSat: 8_000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: false,
AmountInSat: 11_000,
FeeRateInSatsPerVByte: 0,
},
expected: 2.5,
},
{
desc: "TFFA needs 2 coins to spend with debt",
nts: &NextTransactionSize{
SizeProgression: []SizeForAmount{
{
AmountInSat: 10_000,
SizeInVByte: 240,
},
{
AmountInSat: 20_000,
SizeInVByte: 400,
},
},
ExpectedDebtInSat: 8_000,
},
payment: &PaymentToAddress{
TakeFeeFromAmount: true,
AmountInSat: 12_000,
},
expected: 30,
},
}
for _, tC := range testCases {
t.Run(tC.desc, func(t *testing.T) {
var analyzer *PaymentAnalyzer
if tC.nts != nil {
analyzer = NewPaymentAnalyzer(defaultFeeWindow, tC.nts)
} else {
analyzer = NewPaymentAnalyzer(defaultFeeWindow, defaultNTS)
}
maxFeeRate := analyzer.MaxFeeRateToAddress(tC.payment)
if maxFeeRate != tC.expected {
t.Fatalf("Max fee rate %v != %v", maxFeeRate, tC.expected)
}
})
}
}