mirror of
https://github.com/lightninglabs/loop
synced 2024-11-04 06:00:21 +00:00
501 lines
14 KiB
Go
501 lines
14 KiB
Go
package liquidity
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightninglabs/loop"
|
|
"github.com/lightninglabs/loop/loopdb"
|
|
"github.com/lightninglabs/loop/swap"
|
|
"github.com/lightninglabs/loop/test"
|
|
"github.com/lightningnetwork/lnd/clock"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/ticker"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
defaultEventuallyTimeout = time.Second * 45
|
|
defaultEventuallyInterval = time.Millisecond * 100
|
|
)
|
|
|
|
type autoloopTestCtx struct {
|
|
t *testing.T
|
|
manager *Manager
|
|
lnd *test.LndMockServices
|
|
testClock *clock.TestClock
|
|
|
|
// quoteRequests is a channel that requests for quotes are pushed into.
|
|
quoteRequest chan *loop.LoopOutQuoteRequest
|
|
|
|
// quotes is a channel that we get loop out quote requests on.
|
|
quotes chan *loop.LoopOutQuote
|
|
|
|
// quoteRequestIn is a channel that requests for loop in quotes are
|
|
// pushed into.
|
|
quoteRequestIn chan *loop.LoopInQuoteRequest
|
|
|
|
// quotesIn is a channel that we get loop in quote responses on.
|
|
quotesIn chan *loop.LoopInQuote
|
|
|
|
// loopOutRestrictions is a channel that we get the server's
|
|
// restrictions on.
|
|
loopOutRestrictions chan *Restrictions
|
|
|
|
// loopInRestrictions is a channel that we get the server's
|
|
// loop in restrictions on.
|
|
loopInRestrictions chan *Restrictions
|
|
|
|
// loopOuts is a channel that we get existing loop out swaps on.
|
|
loopOuts chan []*loopdb.LoopOut
|
|
|
|
// loopOutSingle is the single loop out returned from fetching a single
|
|
// swap from store.
|
|
loopOutSingle *loopdb.LoopOut
|
|
|
|
// loopIns is a channel that we get existing loop in swaps on.
|
|
loopIns chan []*loopdb.LoopIn
|
|
|
|
// loopInSingle is the single loop in returned from fetching a single
|
|
// swap from store.
|
|
loopInSingle *loopdb.LoopIn
|
|
|
|
// restrictions is a channel that we get swap restrictions on.
|
|
restrictions chan *Restrictions
|
|
|
|
// outRequest is a channel that requests to dispatch loop outs are
|
|
// pushed into.
|
|
outRequest chan *loop.OutRequest
|
|
|
|
// loopOut is a channel that we return loop out responses on.
|
|
loopOut chan *loop.LoopOutSwapInfo
|
|
|
|
// inRequest is a channel that requests to dispatch loop in swaps are
|
|
// pushed into.
|
|
inRequest chan *loop.LoopInRequest
|
|
|
|
// loopIn is a channel that we return loop in responses on.
|
|
loopIn chan *loop.LoopInSwapInfo
|
|
|
|
// errChan is a channel that we send run errors into.
|
|
errChan chan error
|
|
|
|
// cancelCtx cancels the context that our liquidity manager is run with.
|
|
// This can be used to cleanly shutdown the test. Note that this will be
|
|
// nil until the test context has been started.
|
|
cancelCtx func()
|
|
}
|
|
|
|
// newAutoloopTestCtx creates a test context with custom liquidity manager
|
|
// parameters and lnd channels.
|
|
func newAutoloopTestCtx(t *testing.T, parameters Parameters,
|
|
channels []lndclient.ChannelInfo,
|
|
server *Restrictions) *autoloopTestCtx {
|
|
|
|
// Create a mock lnd and set our expected fee rate for sweeps to our
|
|
// sweep fee rate limit value.
|
|
lnd := test.NewMockLnd()
|
|
|
|
categories, ok := parameters.FeeLimit.(*FeeCategoryLimit)
|
|
if ok {
|
|
lnd.SetFeeEstimate(
|
|
parameters.SweepConfTarget, categories.SweepFeeRateLimit,
|
|
)
|
|
}
|
|
|
|
testCtx := &autoloopTestCtx{
|
|
t: t,
|
|
testClock: clock.NewTestClock(testTime),
|
|
lnd: lnd,
|
|
|
|
quoteRequest: make(chan *loop.LoopOutQuoteRequest),
|
|
quotes: make(chan *loop.LoopOutQuote),
|
|
quoteRequestIn: make(chan *loop.LoopInQuoteRequest),
|
|
quotesIn: make(chan *loop.LoopInQuote),
|
|
loopOutRestrictions: make(chan *Restrictions),
|
|
loopInRestrictions: make(chan *Restrictions),
|
|
loopOuts: make(chan []*loopdb.LoopOut),
|
|
loopIns: make(chan []*loopdb.LoopIn),
|
|
restrictions: make(chan *Restrictions),
|
|
outRequest: make(chan *loop.OutRequest),
|
|
loopOut: make(chan *loop.LoopOutSwapInfo),
|
|
inRequest: make(chan *loop.LoopInRequest),
|
|
loopIn: make(chan *loop.LoopInSwapInfo),
|
|
errChan: make(chan error, 1),
|
|
}
|
|
|
|
// Set lnd's channels to equal the set of channels we want for our
|
|
// test.
|
|
testCtx.lnd.Channels = channels
|
|
|
|
cfg := &Config{
|
|
AutoloopTicker: ticker.NewForce(DefaultAutoloopTicker),
|
|
Restrictions: func(_ context.Context, swapType swap.Type, initiator string) (*Restrictions,
|
|
error) {
|
|
|
|
if swapType == swap.TypeOut {
|
|
return <-testCtx.loopOutRestrictions, nil
|
|
}
|
|
|
|
return <-testCtx.loopInRestrictions, nil
|
|
},
|
|
ListLoopOut: func(context.Context) ([]*loopdb.LoopOut, error) {
|
|
return <-testCtx.loopOuts, nil
|
|
},
|
|
GetLoopOut: func(ctx context.Context,
|
|
hash lntypes.Hash) (*loopdb.LoopOut, error) {
|
|
|
|
return testCtx.loopOutSingle, nil
|
|
},
|
|
ListLoopIn: func(context.Context) ([]*loopdb.LoopIn, error) {
|
|
return <-testCtx.loopIns, nil
|
|
},
|
|
LoopOutQuote: func(_ context.Context,
|
|
req *loop.LoopOutQuoteRequest) (*loop.LoopOutQuote,
|
|
error) {
|
|
|
|
testCtx.quoteRequest <- req
|
|
|
|
return <-testCtx.quotes, nil
|
|
},
|
|
LoopOut: func(_ context.Context,
|
|
req *loop.OutRequest) (*loop.LoopOutSwapInfo,
|
|
error) {
|
|
|
|
testCtx.outRequest <- req
|
|
|
|
return <-testCtx.loopOut, nil
|
|
},
|
|
LoopInQuote: func(_ context.Context,
|
|
req *loop.LoopInQuoteRequest) (*loop.LoopInQuote, error) {
|
|
|
|
testCtx.quoteRequestIn <- req
|
|
|
|
return <-testCtx.quotesIn, nil
|
|
},
|
|
LoopIn: func(_ context.Context,
|
|
req *loop.LoopInRequest) (*loop.LoopInSwapInfo, error) {
|
|
|
|
testCtx.inRequest <- req
|
|
|
|
return <-testCtx.loopIn, nil
|
|
},
|
|
MinimumConfirmations: loop.DefaultSweepConfTarget,
|
|
Lnd: &testCtx.lnd.LndServices,
|
|
Clock: testCtx.testClock,
|
|
PutLiquidityParams: func(_ context.Context, _ []byte) error {
|
|
return nil
|
|
},
|
|
FetchLiquidityParams: func(context.Context) ([]byte, error) {
|
|
return nil, nil
|
|
},
|
|
}
|
|
|
|
// SetParameters needs to make a call to our mocked restrictions call,
|
|
// which will block, so we push our test values in a goroutine.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
testCtx.loopOutRestrictions <- server
|
|
close(done)
|
|
}()
|
|
|
|
// Create a manager with our test config and set our starting set of
|
|
// parameters.
|
|
testCtx.manager = NewManager(cfg)
|
|
err := testCtx.manager.setParameters(context.Background(), parameters)
|
|
assert.NoError(t, err)
|
|
// Override the payments check interval for the tests in order to not
|
|
// timeout.
|
|
testCtx.manager.params.CustomPaymentCheckInterval =
|
|
150 * time.Millisecond
|
|
<-done
|
|
return testCtx
|
|
}
|
|
|
|
// start starts our liquidity manager's run loop in a goroutine. Tests should
|
|
// be run with test.Guard() to ensure that this does not leak.
|
|
func (c *autoloopTestCtx) start() {
|
|
ctx := context.Background()
|
|
ctx, c.cancelCtx = context.WithCancel(ctx)
|
|
|
|
go func() {
|
|
c.errChan <- c.manager.Run(ctx)
|
|
}()
|
|
}
|
|
|
|
// stop shuts down our test context and asserts that we have exited with a
|
|
// context-cancelled error.
|
|
func (c *autoloopTestCtx) stop() {
|
|
c.cancelCtx()
|
|
assert.Equal(c.t, context.Canceled, <-c.errChan)
|
|
}
|
|
|
|
// quoteRequestResp pairs an expected swap quote request with the response we
|
|
// would like to provide the liquidity manager with.
|
|
type quoteRequestResp struct {
|
|
request *loop.LoopOutQuoteRequest
|
|
quote *loop.LoopOutQuote
|
|
}
|
|
|
|
// loopOutRequestResp pairs an expected loop out request with the response we
|
|
// would like the server to respond with.
|
|
type loopOutRequestResp struct {
|
|
request *loop.OutRequest
|
|
response *loop.LoopOutSwapInfo
|
|
}
|
|
|
|
// quoteInRequestResp pairs an expected loop in quote request with the response
|
|
// we would like to provide the manager with.
|
|
type quoteInRequestResp struct {
|
|
request *loop.LoopInQuoteRequest
|
|
quote *loop.LoopInQuote
|
|
}
|
|
|
|
// loopInRequestResp pairs and expected loop in request with the response we
|
|
// would like the mocked server to respond with.
|
|
type loopInRequestResp struct {
|
|
request *loop.LoopInRequest
|
|
response *loop.LoopInSwapInfo
|
|
}
|
|
|
|
// autoloopStep contains all of the information to required to step
|
|
// through an autoloop tick.
|
|
type autoloopStep struct {
|
|
minAmt btcutil.Amount
|
|
maxAmt btcutil.Amount
|
|
existingOut []*loopdb.LoopOut
|
|
existingOutSingle *loopdb.LoopOut
|
|
existingIn []*loopdb.LoopIn
|
|
existingInSingle *loopdb.LoopIn
|
|
quotesOut []quoteRequestResp
|
|
quotesIn []quoteInRequestResp
|
|
expectedOut []loopOutRequestResp
|
|
expectedIn []loopInRequestResp
|
|
keepDestAddr bool
|
|
}
|
|
|
|
type easyAutoloopStep struct {
|
|
minAmt btcutil.Amount
|
|
maxAmt btcutil.Amount
|
|
existingOut []*loopdb.LoopOut
|
|
existingIn []*loopdb.LoopIn
|
|
quotesOut []quoteRequestResp
|
|
expectedOut []loopOutRequestResp
|
|
}
|
|
|
|
// autoloop walks our test context through the process of triggering our
|
|
// autoloop functionality, providing mocked values as required. The set of
|
|
// quotes provided indicates that we expect swap suggestions to be made (since
|
|
// we will query for a quote for each suggested swap). The set of expected
|
|
// swaps indicates whether we expect any of these swap suggestions to actually
|
|
// be dispatched by the autolooper.
|
|
func (c *autoloopTestCtx) autoloop(step *autoloopStep) {
|
|
// Tick our autoloop ticker to force assessing whether we want to loop.
|
|
c.manager.cfg.AutoloopTicker.Force <- testTime
|
|
|
|
// Send a mocked response from the server with the swap size limits.
|
|
c.loopOutRestrictions <- NewRestrictions(step.minAmt, step.maxAmt)
|
|
c.loopInRestrictions <- NewRestrictions(step.minAmt, step.maxAmt)
|
|
|
|
// Provide the liquidity manager with our desired existing set of swaps.
|
|
c.loopOuts <- step.existingOut
|
|
c.loopIns <- step.existingIn
|
|
|
|
c.loopOutSingle = step.existingOutSingle
|
|
c.loopInSingle = step.existingInSingle
|
|
|
|
// Assert that we query the server for a quote for each of our
|
|
// recommended swaps. Note that this differs from our set of expected
|
|
// swaps because we may get quotes for suggested swaps but then just
|
|
// log them.
|
|
for _, expected := range step.quotesIn {
|
|
request := <-c.quoteRequestIn
|
|
assert.Equal(
|
|
c.t, expected.request.Amount, request.Amount,
|
|
)
|
|
|
|
assert.Equal(
|
|
c.t, expected.request.HtlcConfTarget,
|
|
request.HtlcConfTarget,
|
|
)
|
|
|
|
c.quotesIn <- expected.quote
|
|
}
|
|
|
|
for _, expected := range step.quotesOut {
|
|
request := <-c.quoteRequest
|
|
assert.Equal(
|
|
c.t, expected.request.Amount, request.Amount,
|
|
)
|
|
assert.Equal(
|
|
c.t, expected.request.SweepConfTarget,
|
|
request.SweepConfTarget,
|
|
)
|
|
c.quotes <- expected.quote
|
|
}
|
|
|
|
require.True(c.t, c.matchLoopOuts(step.expectedOut, step.keepDestAddr))
|
|
require.True(c.t, c.matchLoopIns(step.expectedIn))
|
|
|
|
require.Eventuallyf(c.t, func() bool {
|
|
return c.manager.numActiveStickyLoops() == 0
|
|
}, defaultEventuallyTimeout, defaultEventuallyInterval, "failed to"+
|
|
" wait for sticky loop counter")
|
|
|
|
// Since we're checking if any false-positive swaps were dispatched we
|
|
// need to give some time to autoloop to possibly dispatch them.
|
|
select {
|
|
case <-c.outRequest:
|
|
c.t.Fatal("expected no more loopout requests")
|
|
|
|
case <-c.inRequest:
|
|
c.t.Fatal("expected no more loopin requests")
|
|
|
|
case <-c.quoteRequestIn:
|
|
c.t.Fatal("expected no more loopout quote requests")
|
|
|
|
case <-c.quoteRequest:
|
|
c.t.Fatal("expected no more loopin quote requests")
|
|
|
|
case <-time.After(500 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
// easyautoloop walks our test context through the process of triggering our
|
|
// easy autoloop functionality, providing mocked values as required. The number
|
|
// of values needed to mock easy autoloop are less than standard autoloop as the
|
|
// goal of easy autoloop is to simplify its usage.
|
|
func (c *autoloopTestCtx) easyautoloop(step *easyAutoloopStep, noop bool) {
|
|
// Tick our autoloop ticker to force assessing whether we want to loop.
|
|
c.manager.cfg.AutoloopTicker.Force <- testTime
|
|
|
|
// Provide the liquidity manager with our desired existing set of swaps.
|
|
c.loopOuts <- step.existingOut
|
|
c.loopIns <- step.existingIn
|
|
|
|
// If easy autoloop is not meant to be triggered we skip sending the
|
|
// mock response for restrictions, as this is never called.
|
|
if !noop {
|
|
// Send a mocked response from the server with the swap size limits.
|
|
c.loopOutRestrictions <- NewRestrictions(step.minAmt, step.maxAmt)
|
|
}
|
|
|
|
for _, expected := range step.quotesOut {
|
|
request := <-c.quoteRequest
|
|
require.Equal(
|
|
c.t, expected.request.Amount, request.Amount,
|
|
)
|
|
|
|
c.quotes <- expected.quote
|
|
}
|
|
|
|
for _, expected := range step.expectedOut {
|
|
actual := <-c.outRequest
|
|
|
|
require.Equal(c.t, expected.request.Amount, actual.Amount)
|
|
require.Equal(
|
|
c.t, expected.request.OutgoingChanSet,
|
|
actual.OutgoingChanSet,
|
|
)
|
|
if expected.request.DestAddr != nil {
|
|
require.Equal(
|
|
c.t, expected.request.DestAddr, actual.DestAddr,
|
|
)
|
|
}
|
|
}
|
|
|
|
// Since we're checking if any false-positive swaps were dispatched we
|
|
// need to give some time to autoloop to possibly dispatch them.
|
|
select {
|
|
case <-c.outRequest:
|
|
c.t.Fatal("expected no more loopout requests")
|
|
|
|
case <-c.inRequest:
|
|
c.t.Fatal("expected no more loopin requests")
|
|
|
|
case <-c.quoteRequestIn:
|
|
c.t.Fatal("expected no more loopout quote requests")
|
|
|
|
case <-c.quoteRequest:
|
|
c.t.Fatal("expected no more loopin quote requests")
|
|
|
|
case <-time.After(500 * time.Millisecond):
|
|
}
|
|
}
|
|
|
|
// matchLoopOuts checks that the actual loop out requests we got match the
|
|
// expected ones. The argument keepDestAddr is used to indicate whether we keep
|
|
// the actual loops destination address for the comparison. This is useful
|
|
// because we don't want to compare the destination address generated by the
|
|
// wallet mock. We want to compare the destination address when testing the
|
|
// autoloop DestAddr parameter for loop outs.
|
|
func (c *autoloopTestCtx) matchLoopOuts(swaps []loopOutRequestResp,
|
|
keepDestAddr bool) bool {
|
|
|
|
swapsCopy := make([]loopOutRequestResp, len(swaps))
|
|
copy(swapsCopy, swaps)
|
|
|
|
length := len(swapsCopy)
|
|
|
|
for i := 0; i < length; i++ {
|
|
actual := <-c.outRequest
|
|
|
|
if !keepDestAddr {
|
|
actual.DestAddr = nil
|
|
}
|
|
|
|
inner:
|
|
for index, swap := range swapsCopy {
|
|
equal := reflect.DeepEqual(swap.request, actual)
|
|
|
|
if equal {
|
|
c.loopOut <- swap.response
|
|
|
|
swapsCopy = append(
|
|
swapsCopy[:index],
|
|
swapsCopy[index+1:]...,
|
|
)
|
|
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
|
|
return len(swapsCopy) == 0
|
|
}
|
|
|
|
// matchLoopIns checks that the actual loop in requests we got match the
|
|
// expected ones.
|
|
func (c *autoloopTestCtx) matchLoopIns(
|
|
swaps []loopInRequestResp) bool {
|
|
|
|
swapsCopy := make([]loopInRequestResp, len(swaps))
|
|
copy(swapsCopy, swaps)
|
|
|
|
for i := 0; i < len(swapsCopy); i++ {
|
|
actual := <-c.inRequest
|
|
|
|
inner:
|
|
for i, swap := range swapsCopy {
|
|
equal := reflect.DeepEqual(swap.request, actual)
|
|
|
|
if equal {
|
|
c.loopIn <- swap.response
|
|
|
|
swapsCopy = append(
|
|
swapsCopy[:i], swapsCopy[i+1:]...,
|
|
)
|
|
|
|
break inner
|
|
}
|
|
}
|
|
}
|
|
|
|
return len(swapsCopy) == 0
|
|
}
|