loop/loopd/swapclient_server_test.go
Boris Nagaev e467ac932b
looprpc: expose static autoloop output
Extend the public rpc surface for static autoloop integration
without turning the planner on yet. SuggestSwaps responses can
now carry static-address loop-in requests and the new planner
reason for missing static candidates is mapped over rpc.
2026-05-22 02:20:36 -05:00

1211 lines
30 KiB
Go

package loopd
import (
"context"
"os"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btclog/v2"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/address"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightninglabs/loop/staticaddr/script"
"github.com/lightninglabs/loop/swap"
mock_lnd "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
var (
testnetAddr, _ = btcutil.NewAddressScriptHash(
[]byte{123}, &chaincfg.TestNet3Params,
)
mainnetAddr, _ = btcutil.NewAddressScriptHash(
[]byte{123}, &chaincfg.MainNetParams,
)
nodepubkeyAddr, _ = btcutil.DecodeAddress(
mock_lnd.NewMockLnd().NodePubkey, &chaincfg.MainNetParams,
)
chanID1 = lnwire.NewShortChanIDFromInt(1)
chanID2 = lnwire.NewShortChanIDFromInt(2)
chanID3 = lnwire.NewShortChanIDFromInt(3)
chanID4 = lnwire.NewShortChanIDFromInt(4)
peer1 = route.Vertex{1}
peer2 = route.Vertex{2}
channel1 = lndclient.ChannelInfo{
Active: false,
ChannelID: chanID1.ToUint64(),
PubKeyBytes: peer1,
LocalBalance: 10000,
RemoteBalance: 0,
Capacity: 10000,
}
channel2 = lndclient.ChannelInfo{
Active: true,
ChannelID: chanID2.ToUint64(),
PubKeyBytes: peer2,
LocalBalance: 10000,
RemoteBalance: 0,
Capacity: 10000,
}
channel3 = lndclient.ChannelInfo{
Active: true,
ChannelID: chanID3.ToUint64(),
PubKeyBytes: peer2,
LocalBalance: 10000,
RemoteBalance: 0,
Capacity: 10000,
}
channel4 = lndclient.ChannelInfo{
Active: true,
ChannelID: chanID4.ToUint64(),
PubKeyBytes: peer2,
LocalBalance: 1000,
RemoteBalance: 0,
Capacity: 1000,
}
)
// TestValidateConfTarget tests all failure and success cases for our conf
// target validation function, including the case where we replace a zero
// target with the default provided.
func TestValidateConfTarget(t *testing.T) {
const (
// Various input confirmation values for tests.
zeroConf int32 = 0
oneConf int32 = 1
twoConf int32 = 2
fiveConf int32 = 5
// defaultConf is the default confirmation target we use for
// all tests.
defaultConf = 6
)
tests := []struct {
name string
confTarget int32
expectedTarget int32
expectErr bool
}{
{
name: "zero conf, get default",
confTarget: zeroConf,
expectedTarget: defaultConf,
expectErr: false,
},
{
name: "one conf, get error",
confTarget: oneConf,
expectErr: true,
},
{
name: "two conf, ok",
confTarget: twoConf,
expectedTarget: twoConf,
expectErr: false,
},
{
name: "five conf, ok",
confTarget: fiveConf,
expectedTarget: fiveConf,
expectErr: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
target, err := validateConfTarget(
test.confTarget, defaultConf,
)
if test.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, test.expectedTarget, target)
})
}
}
// TestValidateLoopInRequest tests validation of loop in requests.
func TestValidateLoopInRequest(t *testing.T) {
tests := []struct {
name string
amount int64
numDeposits uint32
external bool
confTarget int32
autoSelectDeposits bool
expectErr bool
expectedTarget int32
}{
{
name: "external and htlc conf set",
amount: 100_000,
external: true,
confTarget: 1,
expectErr: true,
expectedTarget: 0,
},
{
name: "external and no conf",
amount: 100_000,
external: true,
confTarget: 0,
expectErr: false,
expectedTarget: 0,
},
{
name: "not external, zero conf",
amount: 100_000,
external: false,
confTarget: 0,
expectErr: false,
expectedTarget: loop.DefaultHtlcConfTarget,
},
{
name: "not external, bad conf",
amount: 100_000,
external: false,
confTarget: 1,
expectErr: true,
expectedTarget: 0,
},
{
name: "not external, ok conf",
amount: 100_000,
external: false,
confTarget: 5,
expectErr: false,
expectedTarget: 5,
},
{
name: "not external, amount no deposit",
amount: 100_000,
numDeposits: 0,
external: false,
expectErr: false,
expectedTarget: loop.DefaultHtlcConfTarget,
},
{
name: "not external, deposit no amount",
amount: 100_000,
numDeposits: 1,
external: false,
expectErr: false,
},
{
name: "not external, deposit fractional amount",
amount: 100_000,
numDeposits: 1,
external: false,
expectErr: false,
},
{
name: "amount with deposit coin select",
amount: 100_000,
autoSelectDeposits: true,
external: false,
expectErr: false,
},
{
name: "amount with deposit coin select",
numDeposits: 1,
autoSelectDeposits: true,
external: false,
expectErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
external := test.external
conf, err := validateLoopInRequest(
test.confTarget, external, test.numDeposits,
btcutil.Amount(test.amount),
test.autoSelectDeposits,
)
if test.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, test.expectedTarget, conf)
})
}
}
// TestStaticAddressLoopInRejectsReservedLabel verifies that external static
// loop-in requests still reject reserved autoloop labels at the RPC boundary.
func TestStaticAddressLoopInRejectsReservedLabel(t *testing.T) {
logger := btclog.NewSLogger(
btclog.NewDefaultHandler(os.Stdout),
)
setLogger(logger.SubSystem(Subsystem))
server := &swapClientServer{}
_, err := server.StaticAddressLoopIn(
t.Context(), &looprpc.StaticAddressLoopInRequest{
Label: labels.AutoloopLabel(swap.TypeIn),
},
)
require.ErrorContains(t, err, labels.ErrReservedPrefix.Error())
}
// TestRPCAutoloopReasonStaticLoopInNoCandidate verifies that the new planner
// reason is exposed over rpc.
func TestRPCAutoloopReasonStaticLoopInNoCandidate(t *testing.T) {
reason, err := rpcAutoloopReason(
liquidity.ReasonStaticLoopInNoCandidate,
)
require.NoError(t, err)
require.Equal(
t,
looprpc.AutoReason_AUTO_REASON_STATIC_LOOP_IN_NO_CANDIDATE,
reason,
)
}
// TestSwapClientServerStopDaemon ensures that calling StopDaemon triggers the
// daemon shutdown.
func TestSwapClientServerStopDaemon(t *testing.T) {
t.Parallel()
// Prepare a server instance that tracks whether shutdown is requested.
var stopCalled bool
server := &swapClientServer{
stopDaemon: func() {
stopCalled = true
},
}
// Request the daemon to stop and assert the callback executed.
_, err := server.StopDaemon(
context.Background(), &looprpc.StopDaemonRequest{},
)
require.NoError(t, err)
// Ensure our shutdown callback executed.
require.True(t, stopCalled)
}
// TestValidateLoopOutRequest tests validation of loop out requests.
func TestValidateLoopOutRequest(t *testing.T) {
tests := []struct {
name string
chain chaincfg.Params
confTarget int32
destAddr btcutil.Address
label string
channels []lndclient.ChannelInfo
outgoingChanSet []uint64
amount int64
maxRoutingFee int64
maxParts uint32
err error
expectedTarget int32
}{
{
name: "mainnet address with mainnet backend",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel2,
},
amount: 10000,
maxParts: 5,
err: nil,
expectedTarget: 2,
},
{
name: "mainnet address with testnet backend",
chain: chaincfg.TestNet3Params,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel2,
},
amount: 10000,
maxParts: 5,
err: errIncorrectChain,
expectedTarget: 0,
},
{
name: "testnet address with testnet backend",
chain: chaincfg.TestNet3Params,
destAddr: testnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel2,
},
amount: 10000,
maxParts: 5,
err: nil,
expectedTarget: 2,
},
{
name: "testnet address with mainnet backend",
chain: chaincfg.MainNetParams,
destAddr: testnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel2,
},
amount: 10000,
maxParts: 5,
err: errIncorrectChain,
expectedTarget: 0,
},
{
name: "invalid label",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: labels.Reserved,
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel2,
},
amount: 10000,
maxParts: 5,
err: labels.ErrReservedPrefix,
expectedTarget: 0,
},
{
name: "invalid conf target",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 1,
channels: []lndclient.ChannelInfo{
channel2,
},
amount: 10000,
maxParts: 5,
err: errConfTargetTooLow,
expectedTarget: 0,
},
{
name: "default conf target",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 0,
channels: []lndclient.ChannelInfo{
channel2,
},
amount: 10000,
maxParts: 5,
err: nil,
expectedTarget: 9,
},
{
name: "valid amount for default channel set",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel1, channel2, channel3,
},
amount: 20000,
maxParts: 5,
err: nil,
expectedTarget: 2,
},
{
name: "invalid amount for default channel set",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel1, channel2, channel3,
},
amount: 25000,
maxParts: 5,
err: errBalanceTooLow,
expectedTarget: 0,
},
{
name: "inactive channel in outgoing channel set",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel1, channel2, channel3,
},
outgoingChanSet: []uint64{
chanID1.ToUint64(),
},
amount: 1000,
maxParts: 5,
err: errBalanceTooLow,
expectedTarget: 0,
},
{
name: "outgoing channel set balance is enough",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel1, channel2, channel3,
},
outgoingChanSet: []uint64{
chanID2.ToUint64(),
},
amount: 1000,
maxParts: 5,
err: nil,
expectedTarget: 2,
},
{
name: "outgoing channel set balance not sufficient",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel1, channel2, channel3,
},
outgoingChanSet: []uint64{
chanID2.ToUint64(),
},
amount: 20000,
maxParts: 5,
err: errBalanceTooLow,
expectedTarget: 0,
},
{
name: "amount with routing fee too high",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel2,
},
amount: 10000,
maxRoutingFee: 100,
maxParts: 5,
err: errBalanceTooLow,
expectedTarget: 0,
},
{
name: "can split between channels",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel2, channel4,
},
amount: 11000,
maxParts: 16,
err: nil,
expectedTarget: 2,
},
{
name: "can't split between channels",
chain: chaincfg.MainNetParams,
destAddr: mainnetAddr,
label: "label ok",
confTarget: 2,
channels: []lndclient.ChannelInfo{
channel2, channel4,
},
amount: 11000,
maxParts: 5,
err: errBalanceTooLow,
expectedTarget: 0,
},
{
name: "node pubkey as dest addr",
chain: chaincfg.MainNetParams,
destAddr: nodepubkeyAddr,
err: errInvalidAddress,
expectedTarget: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
ctx := context.Background()
lnd := mock_lnd.NewMockLnd()
lnd.Channels = test.channels
req := &looprpc.LoopOutRequest{
Amt: test.amount,
MaxSwapRoutingFee: test.maxRoutingFee,
OutgoingChanSet: test.outgoingChanSet,
Label: test.label,
SweepConfTarget: test.confTarget,
}
logger := btclog.NewSLogger(
btclog.NewDefaultHandler(os.Stdout),
)
setLogger(logger.SubSystem(Subsystem))
conf, err := validateLoopOutRequest(
ctx, lnd.Client, &test.chain, req,
test.destAddr, test.maxParts,
)
require.ErrorIs(t, err, test.err)
require.Equal(t, test.expectedTarget, conf)
})
}
}
// TestHasBandwidth tests that the hasBandwidth function correctly simulates
// the MPP logic used by LND.
func TestHasBandwidth(t *testing.T) {
tests := []struct {
name string
channels []lndclient.ChannelInfo
maxParts int
amt btcutil.Amount
expectedRes bool
expectedShards int
}{
{
name: "can route due to high number of parts",
channels: []lndclient.ChannelInfo{
{
LocalBalance: 100,
},
{
LocalBalance: 10,
},
},
maxParts: 11,
amt: 110,
expectedRes: true,
expectedShards: 8,
},
{
name: "can't route due to low number of parts",
channels: []lndclient.ChannelInfo{
{
LocalBalance: 100,
},
{
LocalBalance: 10,
},
},
maxParts: 5,
amt: 110,
expectedRes: false,
},
{
name: "can route",
channels: []lndclient.ChannelInfo{
{
LocalBalance: 1000,
},
{
LocalBalance: 1000,
},
},
maxParts: 5,
amt: 2000,
expectedRes: true,
expectedShards: 2,
},
{
name: "can route",
channels: []lndclient.ChannelInfo{
{
LocalBalance: 100,
},
{
LocalBalance: 100,
},
{
LocalBalance: 100,
},
},
maxParts: 10,
amt: 300,
expectedRes: true,
expectedShards: 10,
},
{
name: "can't route due to empty channel set",
maxParts: 10,
amt: 300,
expectedRes: false,
expectedShards: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
res, shards := hasBandwidth(test.channels, test.amt,
test.maxParts)
require.Equal(t, test.expectedRes, res)
require.Equal(t, test.expectedShards, shards)
})
}
}
// TestListSwapsFilterAndPagination tests the filtering and
// paging of the ListSwaps command.
func TestListSwapsFilterAndPagination(t *testing.T) {
unixTime := time.Unix(0, 0)
firstSwapStartTime := unixTime.Add(10 * time.Minute)
secondSwapStartTime := unixTime.Add(20 * time.Minute)
thirdSwapStartTime := unixTime.Add(30 * time.Minute)
// Create a set of test swaps of various types which contain the minimal
// viable amount of info to successfully be run through marshallSwap.
swapInOrder0 := loop.SwapInfo{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateInitiated,
Cost: loopdb.SwapCost{},
},
SwapContract: loopdb.SwapContract{
InitiationTime: firstSwapStartTime,
},
LastUpdate: time.Now(),
SwapHash: lntypes.Hash{1},
SwapType: swap.TypeIn,
HtlcAddressP2WSH: testnetAddr,
HtlcAddressP2TR: testnetAddr,
}
swapOutOrder1 := loop.SwapInfo{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateInitiated,
Cost: loopdb.SwapCost{},
},
SwapContract: loopdb.SwapContract{
InitiationTime: secondSwapStartTime,
},
LastUpdate: time.Now(),
SwapHash: lntypes.Hash{2},
SwapType: swap.TypeOut,
HtlcAddressP2WSH: testnetAddr,
HtlcAddressP2TR: testnetAddr,
}
swapOutOrder2 := loop.SwapInfo{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateInitiated,
Cost: loopdb.SwapCost{},
},
SwapContract: loopdb.SwapContract{
InitiationTime: thirdSwapStartTime,
},
LastUpdate: time.Now(),
SwapHash: lntypes.Hash{3},
SwapType: swap.TypeOut,
HtlcAddressP2WSH: testnetAddr,
HtlcAddressP2TR: testnetAddr,
}
mockSwaps := []loop.SwapInfo{swapInOrder0, swapOutOrder1, swapOutOrder2}
tests := []struct {
name string
// Define the mock swaps that will be stored in the mock client.
mockSwaps []loop.SwapInfo
req *looprpc.ListSwapsRequest
// These hashes must be in the correct return order as the response.
expectedReturnedSwaps []lntypes.Hash
expectedNextStartTime int64
}{
{
name: "fetch with defaults",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{},
expectedReturnedSwaps: []lntypes.Hash{
swapInOrder0.SwapHash,
swapOutOrder1.SwapHash,
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with swaptype=loopin filter",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
SwapType: looprpc.ListSwapsFilter_LOOP_IN},
},
expectedReturnedSwaps: []lntypes.Hash{
swapInOrder0.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with swaptype=loopout filter",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
SwapType: looprpc.ListSwapsFilter_LOOP_OUT,
},
},
expectedReturnedSwaps: []lntypes.Hash{
swapOutOrder1.SwapHash,
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with limit",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
MaxSwaps: 2,
},
expectedReturnedSwaps: []lntypes.Hash{
swapInOrder0.SwapHash,
swapOutOrder1.SwapHash,
},
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
},
{
name: "fetch with limit set to default",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
MaxSwaps: 0,
},
expectedReturnedSwaps: []lntypes.Hash{
swapInOrder0.SwapHash,
swapOutOrder1.SwapHash,
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with time filter #1",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
StartTimestampNs: unixTime.Add(25 * time.Minute).UnixNano(),
},
},
expectedReturnedSwaps: []lntypes.Hash{
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with time filter #2",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
StartTimestampNs: unixTime.Add(5 * time.Minute).UnixNano(),
},
},
expectedReturnedSwaps: []lntypes.Hash{
swapInOrder0.SwapHash,
swapOutOrder1.SwapHash,
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with swaptype=loopout filter, time filter, limit set",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
SwapType: looprpc.ListSwapsFilter_LOOP_OUT,
StartTimestampNs: unixTime.Add(15 * time.Minute).UnixNano(),
},
MaxSwaps: 1,
},
expectedReturnedSwaps: []lntypes.Hash{
swapOutOrder1.SwapHash,
},
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
},
{
name: "fetch with time filter, limit set",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
StartTimestampNs: unixTime.UnixNano(),
},
MaxSwaps: 2,
},
expectedReturnedSwaps: []lntypes.Hash{
swapInOrder0.SwapHash,
swapOutOrder1.SwapHash,
},
expectedNextStartTime: secondSwapStartTime.UnixNano() + 1,
},
{
name: "fetch with time filter, limit set 2",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
StartTimestampNs: unixTime.UnixNano(),
},
MaxSwaps: 3,
},
expectedReturnedSwaps: []lntypes.Hash{
swapInOrder0.SwapHash,
swapOutOrder1.SwapHash,
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with time filter edge case 1",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
StartTimestampNs: secondSwapStartTime.UnixNano(),
},
},
expectedReturnedSwaps: []lntypes.Hash{
swapOutOrder1.SwapHash,
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with time filter edge case 2",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
StartTimestampNs: secondSwapStartTime.UnixNano() + 1,
},
},
expectedReturnedSwaps: []lntypes.Hash{
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
{
name: "fetch with time filter edge case 3",
mockSwaps: mockSwaps,
req: &looprpc.ListSwapsRequest{
ListSwapFilter: &looprpc.ListSwapsFilter{
StartTimestampNs: secondSwapStartTime.UnixNano() - 1,
},
},
expectedReturnedSwaps: []lntypes.Hash{
swapOutOrder1.SwapHash,
swapOutOrder2.SwapHash,
},
expectedNextStartTime: 0,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
// Create the swap client server with our mock client.
server := &swapClientServer{
swaps: make(map[lntypes.Hash]loop.SwapInfo),
}
// Populate the server's swap cache with our mock swaps.
for _, swap := range test.mockSwaps {
server.swaps[swap.SwapHash] = swap
}
// Call the ListSwaps method.
resp, err := server.ListSwaps(context.Background(), test.req)
require.NoError(t, err)
require.Len(
t,
resp.Swaps,
len(test.expectedReturnedSwaps),
"incorrect returned count",
)
// Check order of returned swaps is exactly as expected.
for idx, aswap := range resp.Swaps {
newhash, err := lntypes.MakeHash(aswap.GetIdBytes())
require.NoError(t, err)
require.Equal(
t,
test.expectedReturnedSwaps[idx],
newhash,
"iteration order mismatch",
)
}
require.Equal(
t,
test.expectedNextStartTime,
resp.NextStartTime,
"incorrect next start time",
)
})
}
}
// mockAddressStore is a minimal in-memory store for address parameters.
type mockAddressStore struct {
params []*script.Parameters
}
func (s *mockAddressStore) CreateStaticAddress(_ context.Context,
p *script.Parameters) error {
s.params = append(s.params, p)
return nil
}
func (s *mockAddressStore) GetStaticAddress(_ context.Context, _ []byte) (
*script.Parameters, error) {
if len(s.params) == 0 {
return nil, nil
}
return s.params[0], nil
}
func (s *mockAddressStore) GetAllStaticAddresses(_ context.Context) (
[]*script.Parameters, error) {
return s.params, nil
}
// mockDepositStore implements deposit.Store minimally for DepositsForOutpoints.
type mockDepositStore struct {
byOutpoint map[string]*deposit.Deposit
}
func (s *mockDepositStore) CreateDeposit(_ context.Context,
_ *deposit.Deposit) error {
return nil
}
func (s *mockDepositStore) UpdateDeposit(_ context.Context,
_ *deposit.Deposit) error {
return nil
}
func (s *mockDepositStore) GetDeposit(_ context.Context,
_ deposit.ID) (*deposit.Deposit, error) {
return nil, nil
}
func (s *mockDepositStore) DepositForOutpoint(_ context.Context,
outpoint string) (*deposit.Deposit, error) {
if d, ok := s.byOutpoint[outpoint]; ok {
return d, nil
}
return nil, nil
}
func (s *mockDepositStore) AllDeposits(_ context.Context) ([]*deposit.Deposit,
error) {
return nil, nil
}
// TestListUnspentDeposits tests filtering behavior of ListUnspentDeposits.
func TestListUnspentDeposits(t *testing.T) {
ctx := context.Background()
mock := mock_lnd.NewMockLnd()
// Prepare a single static address parameter set.
_, client := mock_lnd.CreateKey(1)
_, server := mock_lnd.CreateKey(2)
pkScript := []byte("pkscript")
addrParams := &script.Parameters{
ClientPubkey: client,
ServerPubkey: server,
Expiry: 10,
PkScript: pkScript,
}
addrStore := &mockAddressStore{params: []*script.Parameters{addrParams}}
// Build an address manager using our mock lnd and fake address store.
addrMgr, err := address.NewManager(&address.ManagerConfig{
Store: addrStore,
WalletKit: mock.WalletKit,
ChainParams: mock.ChainParams,
// ChainNotifier and AddressClient are not needed for this test.
}, 1)
require.NoError(t, err)
// Construct several UTXOs with different confirmation counts.
makeUtxo := func(idx uint32, confs int64) *lnwallet.Utxo {
return &lnwallet.Utxo{
AddressType: lnwallet.TaprootPubkey,
Value: btcutil.Amount(250_000 + int64(idx)),
Confirmations: confs,
PkScript: pkScript,
OutPoint: wire.OutPoint{
Hash: chainhash.Hash{byte(idx + 1)},
Index: idx,
},
}
}
minConfs := int64(deposit.MinConfs)
utxoBelow := makeUtxo(0, minConfs-1) // always included
utxoAt := makeUtxo(1, minConfs) // included only if Deposited
utxoAbove1 := makeUtxo(2, minConfs+1)
utxoAbove2 := makeUtxo(3, minConfs+2)
// Helper to build the deposit manager with specific states.
buildDepositMgr := func(
states map[wire.OutPoint]fsm.StateType) *deposit.Manager {
store := &mockDepositStore{
byOutpoint: make(map[string]*deposit.Deposit),
}
for op, state := range states {
d := &deposit.Deposit{OutPoint: op}
d.SetState(state)
store.byOutpoint[op.String()] = d
}
return deposit.NewManager(&deposit.ManagerConfig{Store: store})
}
// Include below-min-conf and >=min with Deposited; exclude others.
t.Run("below min conf always, Deposited included, others excluded",
func(t *testing.T) {
mock.SetListUnspent([]*lnwallet.Utxo{
utxoBelow, utxoAt, utxoAbove1, utxoAbove2,
})
depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{
utxoAt.OutPoint: deposit.Deposited,
utxoAbove1.OutPoint: deposit.Withdrawn,
utxoAbove2.OutPoint: deposit.LoopingIn,
})
server := &swapClientServer{
staticAddressManager: addrMgr,
depositManager: depMgr,
}
resp, err := server.ListUnspentDeposits(
ctx, &looprpc.ListUnspentDepositsRequest{},
)
require.NoError(t, err)
// Expect utxoBelow and utxoAt only.
require.Len(t, resp.Utxos, 2)
got := map[string]struct{}{}
for _, u := range resp.Utxos {
got[u.Outpoint] = struct{}{}
// Confirm address string is non-empty and the
// same across utxos.
require.NotEmpty(t, u.StaticAddress)
}
_, ok1 := got[utxoBelow.OutPoint.String()]
_, ok2 := got[utxoAt.OutPoint.String()]
require.True(t, ok1)
require.True(t, ok2)
})
// Swap states, now include utxoBelow and utxoAbove1.
t.Run("Deposited on >=min included; non-Deposited excluded",
func(t *testing.T) {
mock.SetListUnspent(
[]*lnwallet.Utxo{
utxoBelow, utxoAt, utxoAbove1,
utxoAbove2,
})
depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{
utxoAt.OutPoint: deposit.Withdrawn,
utxoAbove1.OutPoint: deposit.Deposited,
utxoAbove2.OutPoint: deposit.Withdrawn,
})
server := &swapClientServer{
staticAddressManager: addrMgr,
depositManager: depMgr,
}
resp, err := server.ListUnspentDeposits(
ctx, &looprpc.ListUnspentDepositsRequest{},
)
require.NoError(t, err)
require.Len(t, resp.Utxos, 2)
got := map[string]struct{}{}
for _, u := range resp.Utxos {
got[u.Outpoint] = struct{}{}
}
_, ok1 := got[utxoBelow.OutPoint.String()]
_, ok2 := got[utxoAbove1.OutPoint.String()]
require.True(t, ok1)
require.True(t, ok2)
})
// Confirmed UTXO not present in store should be included.
t.Run("confirmed utxo not in store is included", func(t *testing.T) {
// Only return a confirmed UTXO from lnd and make sure the
// deposit manager/store doesn't know about it.
mock.SetListUnspent([]*lnwallet.Utxo{utxoAbove2})
// Empty store (no states for any outpoint).
depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{})
server := &swapClientServer{
staticAddressManager: addrMgr,
depositManager: depMgr,
}
resp, err := server.ListUnspentDeposits(
ctx, &looprpc.ListUnspentDepositsRequest{},
)
require.NoError(t, err)
// We expect the confirmed UTXO to be included even though it
// doesn't exist in the store yet.
require.Len(t, resp.Utxos, 1)
require.Equal(
t, utxoAbove2.OutPoint.String(), resp.Utxos[0].Outpoint,
)
require.NotEmpty(t, resp.Utxos[0].StaticAddress)
})
}