package loop import ( "context" "fmt" "testing" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/stretchr/testify/require" ) // testSweeper is implementation of sweeper.Sweeper for test. type testSweeper struct { } // GetSweepFeeDetails calculates the required tx fee to spend to destAddr. It // takes a function that is expected to add the weight of the input to the // weight estimator. It returns also the fee rate and transaction weight. func (s testSweeper) GetSweepFeeDetails(ctx context.Context, addInputEstimate func(*input.TxWeightEstimator) error, destAddr btcutil.Address, sweepConfTarget int32, label string) (btcutil.Amount, chainfee.SatPerKWeight, lntypes.WeightUnit, error) { var feeRate chainfee.SatPerKWeight switch { case sweepConfTarget == 0: return 0, 0, 0, fmt.Errorf("zero sweepConfTarget") case sweepConfTarget == 1: return 0, 0, 0, fmt.Errorf("sweepConfTarget is 1") case sweepConfTarget == 2: feeRate = 25000 case sweepConfTarget == 3: feeRate = 20000 case sweepConfTarget < 10: feeRate = 8000 case sweepConfTarget < 100: feeRate = 5000 case sweepConfTarget < 1000: feeRate = 2000 default: feeRate = 250 } // Calculate weight for this tx. var weightEstimate input.TxWeightEstimator // Add output. err := sweep.AddOutputEstimate(&weightEstimate, destAddr) if err != nil { return 0, 0, 0, fmt.Errorf("failed to add output weight "+ "estimate: %w", err) } // Add input. err = addInputEstimate(&weightEstimate) if err != nil { return 0, 0, 0, fmt.Errorf("failed to add input weight "+ "estimate: %w", err) } // Find weight. weight := weightEstimate.Weight() return feeRate.FeeForWeight(weight), feeRate, weight, nil } // TestLoopOutSweepFeerateProvider tests that loopOutSweepFeerateProvider // provides correct fee rate for loop-out swaps. func TestLoopOutSweepFeerateProvider(t *testing.T) { htlcKeys := func() loopdb.HtlcKeys { var senderKey, receiverKey [33]byte // Generate keys. _, senderPubKey := test.CreateKey(1) copy(senderKey[:], senderPubKey.SerializeCompressed()) _, receiverPubKey := test.CreateKey(2) copy(receiverKey[:], receiverPubKey.SerializeCompressed()) return loopdb.HtlcKeys{ SenderScriptKey: senderKey, ReceiverScriptKey: receiverKey, SenderInternalPubKey: senderKey, ReceiverInternalPubKey: receiverKey, } }() var destAddr *btcutil.AddressTaproot swapInvoice := "lntb1230n1pjjszzgpp5j76f03wrkya4sm4gxv6az5nmz5aqsvmn4" + "tpguu2sdvdyygedqjgqdq9xyerxcqzzsxqr23ssp5rwzmwtfjmsgranfk8sr" + "4p4gcgmvyd42uug8pxteg2mkk23ndvkqs9qyyssq44ruk3ex59cmv4dm6k4v" + "0kc6c0gcqjs0gkljfyd6c6uatqa2f67xlx3pcg5tnvcae5p3jju8ra77e87d" + "vhhs0jrx53wnc0fq9rkrhmqqelyx7l" cases := []struct { name string cltvExpiry int32 height int32 amount btcutil.Amount protocolVersion loopdb.ProtocolVersion wantConfTarget int32 wantFeeRate chainfee.SatPerKWeight wantError string }{ { name: "simple case", cltvExpiry: 801_000, height: 800_900, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 100, wantFeeRate: 2000, }, { name: "zero height", cltvExpiry: 801_000, height: 0, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantError: "got zero best block height", }, { name: "huge amount, no proportional fee", cltvExpiry: 801_000, height: 800_900, amount: 100_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 100, wantFeeRate: 2000, }, { name: "huge amount, no proportional fee, v2", cltvExpiry: 801_000, height: 800_900, amount: 100_000_000, protocolVersion: loopdb.ProtocolVersionLoopOutCancel, wantConfTarget: 100, wantFeeRate: 2000, }, { name: "huge amount, no proportional fee, " + "capped by urgent fee", cltvExpiry: 801_000, height: 800_900, amount: 200_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 100, wantFeeRate: 2000, }, { name: "11 blocks until expiry", cltvExpiry: 801_000, height: 800_989, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 11, wantFeeRate: 5000, }, { name: "10 blocks until expiry", cltvExpiry: 801_000, height: 800_990, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 3, wantFeeRate: 22000, }, { name: "9 blocks until expiry", cltvExpiry: 801_000, height: 800_991, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 3, wantFeeRate: 22000, }, { name: "3 blocks until expiry", cltvExpiry: 801_000, height: 800_997, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 3, wantFeeRate: 22000, }, { name: "2 blocks until expiry", cltvExpiry: 801_000, height: 800_998, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 2, wantFeeRate: 27500, }, { name: "1 blocks until expiry", cltvExpiry: 801_000, height: 800_999, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 2, wantFeeRate: 27500, }, { name: "expired", cltvExpiry: 801_000, height: 801_000, amount: 1_000_000, protocolVersion: loopdb.ProtocolVersionMuSig2, wantConfTarget: 2, wantFeeRate: 27500, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { store := loopdb.NewStoreMock(t) ctx := context.Background() swapHash := lntypes.Hash{1, 1, 1} swap := &loopdb.LoopOutContract{ SwapContract: loopdb.SwapContract{ CltvExpiry: tc.cltvExpiry, AmountRequested: tc.amount, ProtocolVersion: tc.protocolVersion, HtlcKeys: htlcKeys, }, DestAddr: destAddr, SwapInvoice: swapInvoice, SweepConfTarget: 100, } err := store.CreateLoopOut(ctx, swapHash, swap) require.NoError(t, err) store.AssertLoopOutStored() getHeight := func() int32 { return tc.height } p := newLoopOutSweepFeerateProvider( testSweeper{}, store, &chaincfg.RegressionNetParams, getHeight, ) confTarget, feeRate, err := p.GetConfTargetAndFeeRate( ctx, swapHash, ) if tc.wantError != "" { require.ErrorContains(t, err, tc.wantError) return } require.NoError(t, err) require.Equal(t, tc.wantConfTarget, confTarget) require.Equal(t, tc.wantFeeRate, feeRate) }) } }