mirror of https://github.com/lightninglabs/loop
multi: add loop in swap builder implementation
parent
b09969e167
commit
5e47a0c6e9
@ -0,0 +1,126 @@
|
|||||||
|
package liquidity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightninglabs/loop"
|
||||||
|
"github.com/lightninglabs/loop/labels"
|
||||||
|
"github.com/lightninglabs/loop/swap"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Compile-time assertion that loopInBuilder satisfies the swapBuilder
|
||||||
|
// interface.
|
||||||
|
var _ swapBuilder = (*loopInBuilder)(nil)
|
||||||
|
|
||||||
|
func newLoopInBuilder(cfg *Config) *loopInBuilder {
|
||||||
|
return &loopInBuilder{
|
||||||
|
cfg: cfg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loopInBuilder struct {
|
||||||
|
// cfg contains all the external functionality we require to create
|
||||||
|
// swaps.
|
||||||
|
cfg *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// swapType returns the swap type that the builder is responsible for creating.
|
||||||
|
func (b *loopInBuilder) swapType() swap.Type {
|
||||||
|
return swap.TypeIn
|
||||||
|
}
|
||||||
|
|
||||||
|
// maySwap checks whether we can currently execute a swap, examining the
|
||||||
|
// current on-chain fee conditions against relevant to our swap type against
|
||||||
|
// our fee restrictions.
|
||||||
|
//
|
||||||
|
// For loop in, we cannot check any upfront costs because we do not know how
|
||||||
|
// many inputs will be used for our on-chain htlc before it is made, so we can't
|
||||||
|
// make nay estimations.
|
||||||
|
func (b *loopInBuilder) maySwap(_ context.Context, _ Parameters) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// inUse examines our current swap traffic to determine whether we should
|
||||||
|
// suggest the builder's type of swap for the peer and channels suggested.
|
||||||
|
func (b *loopInBuilder) inUse(traffic *swapTraffic, peer route.Vertex,
|
||||||
|
channels []lnwire.ShortChannelID) error {
|
||||||
|
|
||||||
|
for _, chanID := range channels {
|
||||||
|
if traffic.ongoingLoopOut[chanID] {
|
||||||
|
log.Debugf("Channel: %v not eligible for suggestions, "+
|
||||||
|
"ongoing loop out utilizing channel", chanID)
|
||||||
|
|
||||||
|
return newReasonError(ReasonLoopOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if traffic.ongoingLoopIn[peer] {
|
||||||
|
log.Debugf("Peer: %x not eligible for suggestions ongoing "+
|
||||||
|
"loop in utilizing peer", peer)
|
||||||
|
|
||||||
|
return newReasonError(ReasonLoopIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastFail, recentFail := traffic.failedLoopIn[peer]
|
||||||
|
if recentFail {
|
||||||
|
log.Debugf("Peer: %v not eligible for suggestions, "+
|
||||||
|
"was part of a failed swap at: %v", peer,
|
||||||
|
lastFail)
|
||||||
|
|
||||||
|
return newReasonError(ReasonFailureBackoff)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildSwap creates a swap for the target peer/channels provided. The autoloop
|
||||||
|
// boolean indicates whether this swap will actually be executed.
|
||||||
|
//
|
||||||
|
// For loop in, we do not add the autoloop label for dry runs.
|
||||||
|
func (b *loopInBuilder) buildSwap(ctx context.Context, pubkey route.Vertex,
|
||||||
|
_ []lnwire.ShortChannelID, amount btcutil.Amount,
|
||||||
|
autoloop bool, params Parameters) (swapSuggestion, error) {
|
||||||
|
|
||||||
|
quote, err := b.cfg.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
|
||||||
|
Amount: amount,
|
||||||
|
LastHop: &pubkey,
|
||||||
|
HtlcConfTarget: params.HtlcConfTarget,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
// If the server fails our quote, we're not reachable right
|
||||||
|
// now, so we want to catch this error and fail with a
|
||||||
|
// structured error so that we know why we can't swap.
|
||||||
|
status, ok := status.FromError(err)
|
||||||
|
if ok && status.Code() == codes.FailedPrecondition {
|
||||||
|
return nil, newReasonError(ReasonLoopInUnreachable)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := params.FeeLimit.loopInLimits(amount, quote); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request := loop.LoopInRequest{
|
||||||
|
Amount: amount,
|
||||||
|
MaxSwapFee: quote.SwapFee,
|
||||||
|
MaxMinerFee: quote.MinerFee,
|
||||||
|
HtlcConfTarget: params.HtlcConfTarget,
|
||||||
|
LastHop: &pubkey,
|
||||||
|
Initiator: autoloopSwapInitiator,
|
||||||
|
}
|
||||||
|
|
||||||
|
if autoloop {
|
||||||
|
request.Label = labels.AutoloopLabel(swap.TypeIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &loopInSwapSuggestion{
|
||||||
|
LoopInRequest: request,
|
||||||
|
}, nil
|
||||||
|
}
|
@ -0,0 +1,193 @@
|
|||||||
|
package liquidity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightninglabs/loop"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestLoopinInUse tests that the loop in swap builder prevents dispatching
|
||||||
|
// swaps for peers when there is already a swap running for that peer.
|
||||||
|
func TestLoopinInUse(t *testing.T) {
|
||||||
|
var (
|
||||||
|
peer1 = route.Vertex{1}
|
||||||
|
chan1 = lnwire.NewShortChanIDFromInt(1)
|
||||||
|
|
||||||
|
peer2 = route.Vertex{2}
|
||||||
|
chan2 = lnwire.NewShortChanIDFromInt(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ongoingLoopOut *lnwire.ShortChannelID
|
||||||
|
ongoingLoopIn *route.Vertex
|
||||||
|
failedLoopIn *route.Vertex
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "swap allowed",
|
||||||
|
ongoingLoopIn: &peer2,
|
||||||
|
ongoingLoopOut: &chan2,
|
||||||
|
failedLoopIn: &peer2,
|
||||||
|
expectedErr: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conflicts with loop out",
|
||||||
|
ongoingLoopOut: &chan1,
|
||||||
|
expectedErr: newReasonError(ReasonLoopOut),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "conflicts with loop in",
|
||||||
|
ongoingLoopIn: &peer1,
|
||||||
|
expectedErr: newReasonError(ReasonLoopIn),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "previous failed loopin",
|
||||||
|
failedLoopIn: &peer1,
|
||||||
|
expectedErr: newReasonError(ReasonFailureBackoff),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range tests {
|
||||||
|
traffic := newSwapTraffic()
|
||||||
|
|
||||||
|
if testCase.ongoingLoopOut != nil {
|
||||||
|
traffic.ongoingLoopOut[*testCase.ongoingLoopOut] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.ongoingLoopIn != nil {
|
||||||
|
traffic.ongoingLoopIn[*testCase.ongoingLoopIn] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if testCase.failedLoopIn != nil {
|
||||||
|
traffic.failedLoopIn[*testCase.failedLoopIn] = testTime
|
||||||
|
}
|
||||||
|
|
||||||
|
builder := newLoopInBuilder(nil)
|
||||||
|
err := builder.inUse(traffic, peer1, []lnwire.ShortChannelID{
|
||||||
|
chan1,
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, testCase.expectedErr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoopinBuildSwap tests construction of loop in swaps for autoloop,
|
||||||
|
// including the case where the client cannot get a quote because it is not
|
||||||
|
// reachable from the server.
|
||||||
|
func TestLoopinBuildSwap(t *testing.T) {
|
||||||
|
var (
|
||||||
|
peer1 = route.Vertex{1}
|
||||||
|
chan1 = lnwire.NewShortChanIDFromInt(1)
|
||||||
|
|
||||||
|
htlcConfTarget int32 = 6
|
||||||
|
swapAmt btcutil.Amount = 100000
|
||||||
|
|
||||||
|
quote = &loop.LoopInQuote{
|
||||||
|
SwapFee: 1,
|
||||||
|
MinerFee: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedSwap = &loopInSwapSuggestion{
|
||||||
|
loop.LoopInRequest{
|
||||||
|
Amount: swapAmt,
|
||||||
|
MaxSwapFee: quote.SwapFee,
|
||||||
|
MaxMinerFee: quote.MinerFee,
|
||||||
|
HtlcConfTarget: htlcConfTarget,
|
||||||
|
LastHop: &peer1,
|
||||||
|
Initiator: autoloopSwapInitiator,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
quoteRequest = &loop.LoopInQuoteRequest{
|
||||||
|
Amount: swapAmt,
|
||||||
|
LastHop: &peer1,
|
||||||
|
HtlcConfTarget: htlcConfTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
errPrecondition = status.Error(codes.FailedPrecondition, "failed")
|
||||||
|
errOtherCode = status.Error(codes.DeadlineExceeded, "timeout")
|
||||||
|
errNoCode = errors.New("failure")
|
||||||
|
)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
prepareMock func(*mockCfg)
|
||||||
|
expectedSwap swapSuggestion
|
||||||
|
expectedErr error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "quote successful",
|
||||||
|
prepareMock: func(m *mockCfg) {
|
||||||
|
m.On(
|
||||||
|
"LoopInQuote", mock.Anything,
|
||||||
|
quoteRequest,
|
||||||
|
).Return(quote, nil)
|
||||||
|
},
|
||||||
|
expectedSwap: expectedSwap,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "client unreachable",
|
||||||
|
prepareMock: func(m *mockCfg) {
|
||||||
|
m.On(
|
||||||
|
"LoopInQuote", mock.Anything,
|
||||||
|
quoteRequest,
|
||||||
|
).Return(quote, errPrecondition)
|
||||||
|
},
|
||||||
|
expectedSwap: nil,
|
||||||
|
expectedErr: newReasonError(ReasonLoopInUnreachable),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other error code",
|
||||||
|
prepareMock: func(m *mockCfg) {
|
||||||
|
m.On(
|
||||||
|
"LoopInQuote", mock.Anything,
|
||||||
|
quoteRequest,
|
||||||
|
).Return(quote, errOtherCode)
|
||||||
|
},
|
||||||
|
expectedSwap: nil,
|
||||||
|
expectedErr: errOtherCode,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no error code",
|
||||||
|
prepareMock: func(m *mockCfg) {
|
||||||
|
m.On(
|
||||||
|
"LoopInQuote", mock.Anything,
|
||||||
|
quoteRequest,
|
||||||
|
).Return(quote, errNoCode)
|
||||||
|
},
|
||||||
|
expectedSwap: nil,
|
||||||
|
expectedErr: errNoCode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range tests {
|
||||||
|
mock, cfg := newMockConfig()
|
||||||
|
params := defaultParameters
|
||||||
|
params.HtlcConfTarget = htlcConfTarget
|
||||||
|
params.AutoFeeBudget = 100000
|
||||||
|
|
||||||
|
testCase.prepareMock(mock)
|
||||||
|
|
||||||
|
builder := newLoopInBuilder(cfg)
|
||||||
|
swap, err := builder.buildSwap(
|
||||||
|
context.Background(), peer1, []lnwire.ShortChannelID{
|
||||||
|
chan1,
|
||||||
|
}, swapAmt, false, params,
|
||||||
|
)
|
||||||
|
assert.Equal(t, testCase.expectedSwap, swap)
|
||||||
|
assert.Equal(t, testCase.expectedErr, err)
|
||||||
|
|
||||||
|
mock.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package liquidity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/lightninglabs/loop"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newMockConfig returns a liquidity config with mocked calls. Note that
|
||||||
|
// functions that are not implemented by the mock will panic if called.
|
||||||
|
func newMockConfig() (*mockCfg, *Config) {
|
||||||
|
mockCfg := &mockCfg{}
|
||||||
|
|
||||||
|
// Create a liquidity config which calls our mock.
|
||||||
|
config := &Config{
|
||||||
|
LoopInQuote: mockCfg.LoopInQuote,
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockCfg, config
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockCfg struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoopInQuote mocks a call to get a loop in quote from the server.
|
||||||
|
func (m *mockCfg) LoopInQuote(ctx context.Context,
|
||||||
|
request *loop.LoopInQuoteRequest) (*loop.LoopInQuote, error) {
|
||||||
|
|
||||||
|
args := m.Called(ctx, request)
|
||||||
|
return args.Get(0).(*loop.LoopInQuote), args.Error(1)
|
||||||
|
}
|
Loading…
Reference in New Issue