mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr: add autoloop loop-in prep
Add the static-address helper that prepares full-deposit autoloop loop-ins without dispatching them. The helper selects no-change deposit sets, records explicit outpoints, and quotes the exact selected amount before the planner tries to dispatch anything. The tests cover the full-deposit selector, the quoted request construction, and excluded outpoint handling so later liquidity work can rely on a stable preparation surface.
This commit is contained in:
parent
48a7bdc7d0
commit
94fc04a71a
3 changed files with 612 additions and 9 deletions
254
staticaddr/loopin/autoloop.go
Normal file
254
staticaddr/loopin/autoloop.go
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrNoAutoloopCandidate is returned when the static-address side
|
||||
// cannot build a full-deposit, no-change loop-in candidate that fits
|
||||
// the planner's requested amount bounds.
|
||||
ErrNoAutoloopCandidate = errors.New("no autoloop candidate")
|
||||
)
|
||||
|
||||
// PrepareAutoloopLoopIn builds a static-address loop-in request for autoloop
|
||||
// without dispatching it. The returned request always uses full deposits,
|
||||
// explicit outpoints, and an explicit selected amount, so the caller can
|
||||
// account for the suggestion without depending on static-address internals.
|
||||
func (m *Manager) PrepareAutoloopLoopIn(ctx context.Context,
|
||||
lastHop route.Vertex, minAmount, maxAmount btcutil.Amount, label,
|
||||
initiator string, excludedOutpoints []string) (
|
||||
*loop.StaticAddressLoopInRequest, int, bool, error) {
|
||||
|
||||
if minAmount <= 0 || maxAmount < minAmount {
|
||||
return nil, 0, false, ErrNoAutoloopCandidate
|
||||
}
|
||||
|
||||
allDeposits, err := m.cfg.DepositManager.GetActiveDepositsInState(
|
||||
deposit.Deposited,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
params, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
excluded := make(map[string]struct{}, len(excludedOutpoints))
|
||||
for _, outpoint := range excludedOutpoints {
|
||||
excluded[outpoint] = struct{}{}
|
||||
}
|
||||
|
||||
selectedDeposits, err := selectNoChangeDeposits(
|
||||
maxAmount, minAmount, allDeposits, params.Expiry,
|
||||
m.currentHeight.Load(), excluded,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
selectedAmount := sumOfDeposits(selectedDeposits)
|
||||
quote, err := m.cfg.QuoteGetter.GetLoopInQuote(
|
||||
ctx, selectedAmount, m.cfg.NodePubkey, &lastHop, nil,
|
||||
initiator, uint32(len(selectedDeposits)), false,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, false, err
|
||||
}
|
||||
|
||||
outpoints := make([]string, 0, len(selectedDeposits))
|
||||
for _, selectedDeposit := range selectedDeposits {
|
||||
outpoints = append(outpoints, selectedDeposit.OutPoint.String())
|
||||
}
|
||||
|
||||
request := &loop.StaticAddressLoopInRequest{
|
||||
DepositOutpoints: outpoints,
|
||||
SelectedAmount: selectedAmount,
|
||||
MaxSwapFee: quote.SwapFee,
|
||||
LastHop: &lastHop,
|
||||
Label: label,
|
||||
Initiator: initiator,
|
||||
Fast: false,
|
||||
}
|
||||
|
||||
return request, len(selectedDeposits), false, nil
|
||||
}
|
||||
|
||||
// selectNoChangeDeposits chooses the highest-value swappable deposit set whose
|
||||
// full value stays within the requested range. The selector never creates
|
||||
// change, so the returned set's total is the actual swap amount.
|
||||
func selectNoChangeDeposits(maxAmount, minAmount btcutil.Amount,
|
||||
unfilteredDeposits []*deposit.Deposit, csvExpiry, blockHeight uint32,
|
||||
excludedOutpoints map[string]struct{}) ([]*deposit.Deposit, error) {
|
||||
|
||||
// Filter out deposits that cannot safely participate in a loop-in or
|
||||
// were already allocated to a larger suggestion earlier in the same
|
||||
// planning pass.
|
||||
deposits := make([]*deposit.Deposit, 0, len(unfilteredDeposits))
|
||||
for _, deposit := range unfilteredDeposits {
|
||||
if _, ok := excludedOutpoints[deposit.OutPoint.String()]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
swappable := IsSwappable(
|
||||
uint32(deposit.ConfirmationHeight), blockHeight,
|
||||
csvExpiry,
|
||||
)
|
||||
if !swappable {
|
||||
continue
|
||||
}
|
||||
|
||||
if deposit.Value > maxAmount {
|
||||
continue
|
||||
}
|
||||
|
||||
deposits = append(deposits, deposit)
|
||||
}
|
||||
|
||||
if len(deposits) == 0 {
|
||||
return nil, ErrNoAutoloopCandidate
|
||||
}
|
||||
|
||||
// Sort by value so the search finds large feasible totals early. The
|
||||
// expiry tie-break keeps equal-value deposits deterministic and helps
|
||||
// the later candidate comparison prefer sooner-expiring funds.
|
||||
sort.SliceStable(deposits, func(i, j int) bool {
|
||||
if deposits[i].Value == deposits[j].Value {
|
||||
return deposits[i].ConfirmationHeight <
|
||||
deposits[j].ConfirmationHeight
|
||||
}
|
||||
|
||||
return deposits[i].Value > deposits[j].Value
|
||||
})
|
||||
|
||||
// Precompute a suffix sum so branches that cannot possibly beat the
|
||||
// current best total can be pruned before exploring the expensive part
|
||||
// of the search tree.
|
||||
suffixSums := make([]btcutil.Amount, len(deposits)+1)
|
||||
for i := len(deposits) - 1; i >= 0; i-- {
|
||||
suffixSums[i] = suffixSums[i+1] + deposits[i].Value
|
||||
}
|
||||
|
||||
var (
|
||||
bestSelection []int
|
||||
bestTotal btcutil.Amount
|
||||
)
|
||||
|
||||
// betterSelection applies the full-deposit ordering:
|
||||
// 1. highest total not exceeding the target
|
||||
// 2. fewer deposits
|
||||
// 3. earlier-expiring deposits
|
||||
betterSelection := func(candidate []int, total btcutil.Amount) bool {
|
||||
switch {
|
||||
case total > bestTotal:
|
||||
return true
|
||||
|
||||
case total < bestTotal:
|
||||
return false
|
||||
|
||||
case bestSelection == nil:
|
||||
return true
|
||||
|
||||
case len(candidate) < len(bestSelection):
|
||||
return true
|
||||
|
||||
case len(candidate) > len(bestSelection):
|
||||
return false
|
||||
}
|
||||
|
||||
// Use signed arithmetic here so an expired deposit cannot wrap
|
||||
// the residual-life comparison if height updates race the
|
||||
// earlier swappability filter.
|
||||
left := make([]int64, len(candidate))
|
||||
for i, index := range candidate {
|
||||
left[i] = deposits[index].ConfirmationHeight +
|
||||
int64(csvExpiry) - int64(blockHeight)
|
||||
}
|
||||
|
||||
right := make([]int64, len(bestSelection))
|
||||
for i, index := range bestSelection {
|
||||
right[i] = deposits[index].ConfirmationHeight +
|
||||
int64(csvExpiry) - int64(blockHeight)
|
||||
}
|
||||
|
||||
slices.Sort(left)
|
||||
slices.Sort(right)
|
||||
|
||||
for i := range left {
|
||||
if left[i] == right[i] {
|
||||
continue
|
||||
}
|
||||
|
||||
return left[i] < right[i]
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// search explores include/exclude choices. The branch-and-bound checks
|
||||
// are intentionally conservative: they only prune when no combination
|
||||
// below the current node can beat the best known total or tie it with a
|
||||
// smaller deposit count.
|
||||
var search func(index int, total btcutil.Amount, selected []int)
|
||||
search = func(index int, total btcutil.Amount, selected []int) {
|
||||
if total > maxAmount {
|
||||
return
|
||||
}
|
||||
|
||||
if total >= minAmount && betterSelection(selected, total) {
|
||||
bestTotal = total
|
||||
bestSelection = append([]int(nil), selected...)
|
||||
}
|
||||
|
||||
if index == len(deposits) {
|
||||
return
|
||||
}
|
||||
|
||||
maxReachable := total + suffixSums[index]
|
||||
if maxReachable < bestTotal {
|
||||
return
|
||||
}
|
||||
|
||||
if maxReachable == bestTotal && bestSelection != nil &&
|
||||
len(selected) >= len(bestSelection) {
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The include branch must not reuse selected's backing array.
|
||||
// Otherwise a later append can leak into the exclude branch
|
||||
// when the slice still has spare capacity.
|
||||
selectedWithIndex := make([]int, len(selected)+1)
|
||||
copy(selectedWithIndex, selected)
|
||||
selectedWithIndex[len(selected)] = index
|
||||
|
||||
search(
|
||||
index+1, total+deposits[index].Value,
|
||||
selectedWithIndex,
|
||||
)
|
||||
search(index+1, total, selected)
|
||||
}
|
||||
|
||||
search(0, 0, nil)
|
||||
|
||||
if len(bestSelection) == 0 {
|
||||
return nil, ErrNoAutoloopCandidate
|
||||
}
|
||||
|
||||
selectedDeposits := make([]*deposit.Deposit, 0, len(bestSelection))
|
||||
for _, index := range bestSelection {
|
||||
selectedDeposits = append(selectedDeposits, deposits[index])
|
||||
}
|
||||
|
||||
return selectedDeposits, nil
|
||||
}
|
||||
330
staticaddr/loopin/autoloop_test.go
Normal file
330
staticaddr/loopin/autoloop_test.go
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
package loopin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightninglabs/loop"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/lightninglabs/loop/staticaddr/script"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSelectNoChangeDeposits verifies the full-deposit static-autoloop
|
||||
// selector. The cases below target the filter paths, the branch-and-bound
|
||||
// search, and every documented tie-breaker explicitly so coverage tracks the
|
||||
// actual selection behavior instead of a handful of happy-path examples.
|
||||
func TestSelectNoChangeDeposits(t *testing.T) {
|
||||
depositSeven := makeDeposit(7, 0, 7_000, 200)
|
||||
depositFour := makeDeposit(4, 0, 4_000, 210)
|
||||
depositThreeA := makeDeposit(3, 0, 3_000, 220)
|
||||
depositThreeB := makeDeposit(9, 0, 3_000, 221)
|
||||
depositNine := makeDeposit(8, 0, 9_000, 205)
|
||||
depositFourA := makeDeposit(5, 0, 4_000, 215)
|
||||
depositFourB := makeDeposit(6, 0, 4_000, 216)
|
||||
depositOneA := makeDeposit(10, 0, 1_000, 230)
|
||||
depositOneB := makeDeposit(11, 0, 1_000, 231)
|
||||
depositOneC := makeDeposit(21, 0, 1_000, 232)
|
||||
depositFourC := makeDeposit(13, 0, 4_000, 200)
|
||||
depositFourD := makeDeposit(14, 0, 4_000, 201)
|
||||
depositFourE := makeDeposit(15, 0, 4_000, 220)
|
||||
depositFourF := makeDeposit(16, 0, 4_000, 221)
|
||||
depositFive := makeDeposit(17, 0, 5_000, 200)
|
||||
depositUnsuitable := makeDeposit(18, 0, 6_000, 149)
|
||||
depositOversized := makeDeposit(19, 0, 9_000, 220)
|
||||
depositTwo := makeDeposit(20, 0, 2_000, 210)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
maxAmount btcutil.Amount
|
||||
minAmount btcutil.Amount
|
||||
deposits []*deposit.Deposit
|
||||
csvExpiry uint32
|
||||
blockHeight uint32
|
||||
excludedOutpoint map[string]struct{}
|
||||
expected []*deposit.Deposit
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "prefers exact deposit over smaller combo",
|
||||
maxAmount: 7_000,
|
||||
minAmount: 3_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositSeven, depositFour, depositThreeA,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expected: []*deposit.Deposit{depositSeven},
|
||||
},
|
||||
{
|
||||
name: "excluded outpoint falls back to combo",
|
||||
maxAmount: 7_000,
|
||||
minAmount: 3_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositSeven, depositFour, depositThreeA,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
excludedOutpoint: map[string]struct{}{
|
||||
depositSeven.OutPoint.String(): {},
|
||||
},
|
||||
expected: []*deposit.Deposit{
|
||||
depositFour, depositThreeA,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same total prefers fewer deposits",
|
||||
maxAmount: 6_000,
|
||||
minAmount: 6_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositFour, depositThreeA, depositThreeB,
|
||||
depositOneA, depositOneB,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expected: []*deposit.Deposit{
|
||||
depositThreeA, depositThreeB,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same total rejects more deposits",
|
||||
maxAmount: 2_000,
|
||||
minAmount: 2_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositTwo, depositOneA,
|
||||
depositOneB, depositOneC,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expected: []*deposit.Deposit{depositTwo},
|
||||
},
|
||||
{
|
||||
name: "same total prefers earlier expiries",
|
||||
maxAmount: 8_000,
|
||||
minAmount: 8_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositFourC, depositFourD,
|
||||
depositFourE, depositFourF,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expected: []*deposit.Deposit{
|
||||
depositFourC, depositFourD,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "identical residual lives keep stable pick",
|
||||
maxAmount: 8_000,
|
||||
minAmount: 8_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositFour, depositThreeA,
|
||||
depositFourA, depositThreeB,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expected: []*deposit.Deposit{
|
||||
depositFour, depositFourA,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "filters unswappable and oversized deposits",
|
||||
maxAmount: 7_000,
|
||||
minAmount: 5_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositFive, depositUnsuitable,
|
||||
depositOversized,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expected: []*deposit.Deposit{depositFive},
|
||||
},
|
||||
{
|
||||
name: "returns no candidate when all are filtered",
|
||||
maxAmount: 7_000,
|
||||
minAmount: 5_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositUnsuitable, depositOversized,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expectedErr: ErrNoAutoloopCandidate,
|
||||
},
|
||||
{
|
||||
name: "returns no candidate below minimum",
|
||||
maxAmount: 10_000,
|
||||
minAmount: 7_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositFour, depositTwo,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expectedErr: ErrNoAutoloopCandidate,
|
||||
},
|
||||
{
|
||||
name: "zero minimum finds best positive total",
|
||||
maxAmount: 7_000,
|
||||
minAmount: 0,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositFour, depositThreeA,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expected: []*deposit.Deposit{
|
||||
depositFour, depositThreeA,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "deeper search isolates include and exclude",
|
||||
maxAmount: 13_000,
|
||||
minAmount: 10_000,
|
||||
deposits: []*deposit.Deposit{
|
||||
depositNine, depositSeven,
|
||||
depositFourA, depositFourB,
|
||||
depositThreeA,
|
||||
},
|
||||
csvExpiry: 1_000,
|
||||
blockHeight: 100,
|
||||
expected: []*deposit.Deposit{
|
||||
depositNine, depositFourA,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
selectedOutpoints := func(deposits []*deposit.Deposit) []string {
|
||||
result := make([]string, 0, len(deposits))
|
||||
for _, selectedDeposit := range deposits {
|
||||
result = append(
|
||||
result, selectedDeposit.OutPoint.String(),
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
selectedDeposits, err := selectNoChangeDeposits(
|
||||
testCase.maxAmount, testCase.minAmount,
|
||||
testCase.deposits, testCase.csvExpiry,
|
||||
testCase.blockHeight, testCase.excludedOutpoint,
|
||||
)
|
||||
|
||||
if testCase.expectedErr != nil {
|
||||
require.ErrorIs(t, err, testCase.expectedErr)
|
||||
require.Nil(t, selectedDeposits)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(
|
||||
t, selectedOutpoints(testCase.expected),
|
||||
selectedOutpoints(selectedDeposits),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrepareAutoloopLoopIn ensures the static manager returns an explicit
|
||||
// full-deposit request and quotes it with the correct amount and deposit
|
||||
// count.
|
||||
func TestPrepareAutoloopLoopIn(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
selectedDeposit := makeDeposit(1, 0, 9_000, 300)
|
||||
|
||||
quoteGetter := &mockQuoteGetter{
|
||||
quote: &loop.LoopInQuote{
|
||||
SwapFee: 123,
|
||||
},
|
||||
}
|
||||
|
||||
manager, err := NewManager(&Config{
|
||||
AddressManager: &mockAddressManager{
|
||||
params: &script.Parameters{
|
||||
Expiry: 1_000,
|
||||
},
|
||||
},
|
||||
DepositManager: &mockDepositManager{
|
||||
activeDeposits: []*deposit.Deposit{selectedDeposit},
|
||||
},
|
||||
QuoteGetter: quoteGetter,
|
||||
NodePubkey: route.Vertex{2},
|
||||
}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
lastHop := route.Vertex{9}
|
||||
request, numDeposits, hasChange, err := manager.PrepareAutoloopLoopIn(
|
||||
ctx, lastHop, 5_000, 10_000, "label", "autoloop", nil,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(
|
||||
t, []string{selectedDeposit.OutPoint.String()},
|
||||
request.DepositOutpoints,
|
||||
)
|
||||
require.Equal(t, selectedDeposit.Value, request.SelectedAmount)
|
||||
require.Equal(t, btcutil.Amount(123), request.MaxSwapFee)
|
||||
require.NotNil(t, request.LastHop)
|
||||
require.Equal(t, lastHop, *request.LastHop)
|
||||
require.Equal(t, "label", request.Label)
|
||||
require.Equal(t, "autoloop", request.Initiator)
|
||||
require.False(t, request.Fast)
|
||||
require.Equal(t, 1, numDeposits)
|
||||
require.False(t, hasChange)
|
||||
|
||||
require.Equal(t, selectedDeposit.Value, quoteGetter.amount)
|
||||
require.NotNil(t, quoteGetter.lastHop)
|
||||
require.Equal(t, lastHop, *quoteGetter.lastHop)
|
||||
require.Equal(t, "autoloop", quoteGetter.initiator)
|
||||
require.Equal(t, uint32(1), quoteGetter.numDeposits)
|
||||
require.False(t, quoteGetter.fast)
|
||||
}
|
||||
|
||||
// TestPrepareAutoloopLoopInExcludedOutpoints verifies that the manager passes
|
||||
// excluded outpoints through the end-to-end preparation path before quoting
|
||||
// the candidate.
|
||||
func TestPrepareAutoloopLoopInExcludedOutpoints(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
excludedDeposit := makeDeposit(1, 0, 9_000, 300)
|
||||
|
||||
selectedDeposit := makeDeposit(2, 0, 7_000, 301)
|
||||
|
||||
quoteGetter := &mockQuoteGetter{
|
||||
quote: &loop.LoopInQuote{
|
||||
SwapFee: 77,
|
||||
},
|
||||
}
|
||||
|
||||
manager, err := NewManager(&Config{
|
||||
AddressManager: &mockAddressManager{
|
||||
params: &script.Parameters{
|
||||
Expiry: 1_000,
|
||||
},
|
||||
},
|
||||
DepositManager: &mockDepositManager{
|
||||
activeDeposits: []*deposit.Deposit{
|
||||
excludedDeposit, selectedDeposit,
|
||||
},
|
||||
},
|
||||
QuoteGetter: quoteGetter,
|
||||
NodePubkey: route.Vertex{2},
|
||||
}, 200)
|
||||
require.NoError(t, err)
|
||||
|
||||
request, numDeposits, hasChange, err := manager.PrepareAutoloopLoopIn(
|
||||
ctx, route.Vertex{9}, 5_000, 10_000, "label", "autoloop",
|
||||
[]string{excludedDeposit.OutPoint.String()},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(
|
||||
t, []string{selectedDeposit.OutPoint.String()},
|
||||
request.DepositOutpoints,
|
||||
)
|
||||
require.Equal(t, selectedDeposit.Value, request.SelectedAmount)
|
||||
require.Equal(t, btcutil.Amount(77), request.MaxSwapFee)
|
||||
require.Equal(t, 1, numDeposits)
|
||||
require.False(t, hasChange)
|
||||
require.Equal(t, selectedDeposit.Value, quoteGetter.amount)
|
||||
}
|
||||
|
|
@ -222,6 +222,9 @@ func TestInitiateLoopInAllowsReservedAutoloopLabel(t *testing.T) {
|
|||
|
||||
// mockDepositManager implements DepositManager for tests.
|
||||
type mockDepositManager struct {
|
||||
// activeDeposits is the set returned by GetActiveDepositsInState.
|
||||
activeDeposits []*deposit.Deposit
|
||||
|
||||
// byOutpoint maps outpoint strings to deposits for direct lookups.
|
||||
byOutpoint map[string]*deposit.Deposit
|
||||
}
|
||||
|
|
@ -277,36 +280,52 @@ func (m *mockDepositManager) DepositsForOutpoints(_ context.Context,
|
|||
func (m *mockDepositManager) GetActiveDepositsInState(_ fsm.StateType) (
|
||||
[]*deposit.Deposit, error) {
|
||||
|
||||
return nil, nil
|
||||
return m.activeDeposits, nil
|
||||
}
|
||||
|
||||
// mockQuoteGetter returns either a configured quote or a configured error and
|
||||
// records the quoted amount for assertions.
|
||||
// mockQuoteGetter records the inputs to quote requests and returns a fixed
|
||||
// loop-in quote.
|
||||
type mockQuoteGetter struct {
|
||||
// quote is the response returned from GetLoopInQuote.
|
||||
quote *loop.LoopInQuote
|
||||
|
||||
// err is the optional error returned from GetLoopInQuote.
|
||||
err error
|
||||
|
||||
// amount records the quoted amount.
|
||||
amount btcutil.Amount
|
||||
|
||||
// lastHop records the quoted last hop.
|
||||
lastHop *route.Vertex
|
||||
|
||||
// initiator records the quoted initiator string.
|
||||
initiator string
|
||||
|
||||
// numDeposits records the quoted deposit count.
|
||||
numDeposits uint32
|
||||
|
||||
// fast records the quoted fast flag.
|
||||
fast bool
|
||||
}
|
||||
|
||||
// GetLoopInQuote returns the configured quote result for tests.
|
||||
// GetLoopInQuote returns the configured quote and records the request
|
||||
// parameters for assertions.
|
||||
func (m *mockQuoteGetter) GetLoopInQuote(_ context.Context,
|
||||
amt btcutil.Amount, _ route.Vertex, lastHop *route.Vertex,
|
||||
_ [][]zpay32.HopHint, initiator string, numDeposits uint32,
|
||||
fast bool) (*loop.LoopInQuote, error) {
|
||||
|
||||
m.amount = amt
|
||||
_ = lastHop
|
||||
_ = initiator
|
||||
_ = numDeposits
|
||||
_ = fast
|
||||
m.lastHop = lastHop
|
||||
m.initiator = initiator
|
||||
m.numDeposits = numDeposits
|
||||
m.fast = fast
|
||||
|
||||
if m.err != nil {
|
||||
return nil, m.err
|
||||
}
|
||||
|
||||
return &loop.LoopInQuote{}, nil
|
||||
return m.quote, nil
|
||||
}
|
||||
|
||||
// mockStore implements StaticAddressLoopInStore for tests.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue