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