mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
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.
657 lines
17 KiB
Go
657 lines
17 KiB
Go
package loopin
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightninglabs/loop"
|
|
"github.com/lightninglabs/loop/fsm"
|
|
"github.com/lightninglabs/loop/labels"
|
|
"github.com/lightninglabs/loop/staticaddr/deposit"
|
|
"github.com/lightninglabs/loop/staticaddr/script"
|
|
"github.com/lightninglabs/loop/swap"
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
"github.com/lightningnetwork/lnd/zpay32"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
type testCase struct {
|
|
name string
|
|
deposits []*deposit.Deposit
|
|
targetValue btcutil.Amount
|
|
csvExpiry uint32
|
|
blockHeight uint32
|
|
expected []*deposit.Deposit
|
|
expectedErr string
|
|
}
|
|
|
|
// TestSelectDeposits tests the selectDeposits function, which selects
|
|
// deposits that can cover a target value while respecting the dust limit.
|
|
func TestSelectDeposits(t *testing.T) {
|
|
d1, d2, d3, d4 := &deposit.Deposit{
|
|
Value: 1_000_000,
|
|
ConfirmationHeight: 5_000,
|
|
}, &deposit.Deposit{
|
|
Value: 2_000_000,
|
|
ConfirmationHeight: 5_001,
|
|
}, &deposit.Deposit{
|
|
Value: 3_000_000,
|
|
ConfirmationHeight: 5_002,
|
|
}, &deposit.Deposit{
|
|
Value: 3_000_000,
|
|
ConfirmationHeight: 5_003,
|
|
}
|
|
d1.Hash = chainhash.Hash{1}
|
|
d1.Index = 0
|
|
d2.Hash = chainhash.Hash{2}
|
|
d2.Index = 0
|
|
d3.Hash = chainhash.Hash{3}
|
|
d3.Index = 0
|
|
d4.Hash = chainhash.Hash{4}
|
|
d4.Index = 0
|
|
|
|
testCases := []testCase{
|
|
{
|
|
name: "single deposit exact target",
|
|
deposits: []*deposit.Deposit{d1},
|
|
targetValue: 1_000_000,
|
|
expected: []*deposit.Deposit{d1},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "prefer larger deposit when both cover",
|
|
deposits: []*deposit.Deposit{d1, d2},
|
|
targetValue: 1_000_000,
|
|
expected: []*deposit.Deposit{d2},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "prefer largest among three when one is enough",
|
|
deposits: []*deposit.Deposit{d1, d2, d3},
|
|
targetValue: 1_000_000,
|
|
expected: []*deposit.Deposit{d3},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "single deposit insufficient by 1",
|
|
deposits: []*deposit.Deposit{d1},
|
|
targetValue: 1_000_001,
|
|
expected: []*deposit.Deposit{},
|
|
expectedErr: "not enough deposits to cover",
|
|
},
|
|
{
|
|
name: "target leaves exact dust limit change",
|
|
deposits: []*deposit.Deposit{d1},
|
|
targetValue: 1_000_000 - dustLimit,
|
|
expected: []*deposit.Deposit{d1},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "target leaves dust change (just over)",
|
|
deposits: []*deposit.Deposit{d1},
|
|
targetValue: 1_000_000 - dustLimit + 1,
|
|
expected: []*deposit.Deposit{},
|
|
expectedErr: "not enough deposits to cover",
|
|
},
|
|
{
|
|
name: "all deposits exactly match target",
|
|
deposits: []*deposit.Deposit{d1, d2, d3},
|
|
targetValue: d1.Value + d2.Value + d3.Value,
|
|
expected: []*deposit.Deposit{d1, d2, d3},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "sum minus dust limit is allowed (change == dust)",
|
|
deposits: []*deposit.Deposit{d1, d2, d3},
|
|
targetValue: d1.Value + d2.Value + d3.Value - dustLimit,
|
|
expected: []*deposit.Deposit{d1, d2, d3},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "sum minus dust limit plus 1 is not allowed (dust change)",
|
|
deposits: []*deposit.Deposit{d1, d2, d3},
|
|
targetValue: d1.Value + d2.Value + d3.Value - dustLimit + 1,
|
|
expected: []*deposit.Deposit{},
|
|
expectedErr: "not enough deposits to cover",
|
|
},
|
|
{
|
|
name: "tie by value, prefer earlier expiry",
|
|
deposits: []*deposit.Deposit{d3, d4},
|
|
targetValue: d4.Value - dustLimit, // d3/d4 have the
|
|
// same value but different expiration.
|
|
expected: []*deposit.Deposit{d3},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
name: "prefilter filters deposits close to expiry",
|
|
deposits: func() []*deposit.Deposit {
|
|
// dClose expires before
|
|
// htlcExpiry+DepositHtlcDelta and must be
|
|
// filtered out. dOK expires exactly at the
|
|
// threshold and must be eligible.
|
|
dClose := &deposit.Deposit{
|
|
Value: 3_000_000,
|
|
ConfirmationHeight: 3000,
|
|
}
|
|
dClose.Hash = chainhash.Hash{5}
|
|
dClose.Index = 0
|
|
dOK := &deposit.Deposit{
|
|
Value: 2_000_000,
|
|
ConfirmationHeight: 3050,
|
|
}
|
|
dOK.Hash = chainhash.Hash{6}
|
|
dOK.Index = 0
|
|
return []*deposit.Deposit{dClose, dOK}
|
|
}(),
|
|
targetValue: 1_000_000,
|
|
csvExpiry: 1000,
|
|
blockHeight: 3000,
|
|
expected: func() []*deposit.Deposit {
|
|
// Only dOK should be considered.
|
|
// dClose is filtered.
|
|
dOK := &deposit.Deposit{
|
|
Value: 2_000_000,
|
|
ConfirmationHeight: 3050,
|
|
}
|
|
dOK.Hash = chainhash.Hash{6}
|
|
dOK.Index = 0
|
|
return []*deposit.Deposit{dOK}
|
|
}(),
|
|
expectedErr: "",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
selectedDeposits, err := SelectDeposits(
|
|
tc.targetValue, tc.deposits, tc.csvExpiry,
|
|
tc.blockHeight,
|
|
)
|
|
if tc.expectedErr == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, tc.expectedErr)
|
|
}
|
|
require.ElementsMatch(t, tc.expected, selectedDeposits)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestInitiateLoopInAllowsReservedAutoloopLabel verifies that the internal
|
|
// loop-in manager path does not reject reserved autoloop labels. The RPC
|
|
// boundary owns that validation, while internal autoloop dispatch must be able
|
|
// to reuse the reserved labels directly.
|
|
func TestInitiateLoopInAllowsReservedAutoloopLabel(t *testing.T) {
|
|
ctx := t.Context()
|
|
|
|
const confirmationHeight = 0
|
|
selectedDeposit := makeDeposit(1, 0, 9_000, confirmationHeight)
|
|
selectedOutpoint := selectedDeposit.OutPoint.String()
|
|
quoteErr := errors.New("quote failed")
|
|
quoteGetter := &mockQuoteGetter{
|
|
err: quoteErr,
|
|
}
|
|
|
|
manager, err := NewManager(&Config{
|
|
DepositManager: &mockDepositManager{
|
|
byOutpoint: map[string]*deposit.Deposit{
|
|
selectedOutpoint: selectedDeposit,
|
|
},
|
|
},
|
|
QuoteGetter: quoteGetter,
|
|
NodePubkey: route.Vertex{2},
|
|
}, 200)
|
|
require.NoError(t, err)
|
|
|
|
_, err = manager.initiateLoopIn(ctx, &loop.StaticAddressLoopInRequest{
|
|
DepositOutpoints: []string{selectedOutpoint},
|
|
SelectedAmount: selectedDeposit.Value,
|
|
MaxSwapFee: 1_000,
|
|
Label: labels.AutoloopLabel(swap.TypeIn),
|
|
Initiator: "autoloop",
|
|
})
|
|
require.ErrorIs(t, err, quoteErr)
|
|
require.NotContains(t, err.Error(), labels.ErrReservedPrefix.Error())
|
|
require.Equal(t, selectedDeposit.Value, quoteGetter.amount)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (m *mockDepositManager) GetAllDeposits(_ context.Context) (
|
|
[]*deposit.Deposit, error) {
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *mockDepositManager) AllStringOutpointsActiveDeposits(outpoints []string,
|
|
state fsm.StateType) ([]*deposit.Deposit, bool) {
|
|
|
|
if state != deposit.Deposited {
|
|
return nil, false
|
|
}
|
|
|
|
if m.byOutpoint == nil {
|
|
return nil, false
|
|
}
|
|
|
|
res := make([]*deposit.Deposit, 0, len(outpoints))
|
|
for _, outpoint := range outpoints {
|
|
selectedDeposit, ok := m.byOutpoint[outpoint]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
|
|
res = append(res, selectedDeposit)
|
|
}
|
|
|
|
return res, true
|
|
}
|
|
|
|
func (m *mockDepositManager) TransitionDeposits(_ context.Context,
|
|
_ []*deposit.Deposit, _ fsm.EventType, _ fsm.StateType) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *mockDepositManager) DepositsForOutpoints(_ context.Context,
|
|
outpoints []string, ignoreUnknown bool) ([]*deposit.Deposit, error) {
|
|
|
|
res := make([]*deposit.Deposit, 0, len(outpoints))
|
|
for _, op := range outpoints {
|
|
if d, ok := m.byOutpoint[op]; ok {
|
|
res = append(res, d)
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func (m *mockDepositManager) GetActiveDepositsInState(_ fsm.StateType) (
|
|
[]*deposit.Deposit, error) {
|
|
|
|
return m.activeDeposits, nil
|
|
}
|
|
|
|
// 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 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
|
|
m.lastHop = lastHop
|
|
m.initiator = initiator
|
|
m.numDeposits = numDeposits
|
|
m.fast = fast
|
|
|
|
if m.err != nil {
|
|
return nil, m.err
|
|
}
|
|
|
|
return m.quote, nil
|
|
}
|
|
|
|
// mockStore implements StaticAddressLoopInStore for tests.
|
|
type mockStore struct {
|
|
loopIns map[lntypes.Hash]*StaticAddressLoopIn
|
|
mapIDs map[lntypes.Hash][]deposit.ID
|
|
}
|
|
|
|
func (s *mockStore) CreateLoopIn(_ context.Context,
|
|
_ *StaticAddressLoopIn) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *mockStore) UpdateLoopIn(_ context.Context,
|
|
_ *StaticAddressLoopIn) error {
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *mockStore) GetStaticAddressLoopInSwapsByStates(_ context.Context,
|
|
_ []fsm.StateType) ([]*StaticAddressLoopIn, error) {
|
|
|
|
return nil, nil
|
|
}
|
|
func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) {
|
|
return false, nil
|
|
}
|
|
|
|
func (s *mockStore) GetLoopInByHash(_ context.Context,
|
|
swapHash lntypes.Hash) (*StaticAddressLoopIn, error) {
|
|
|
|
li, ok := s.loopIns[swapHash]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
return li, nil
|
|
}
|
|
func (s *mockStore) SwapHashesForDepositIDs(_ context.Context,
|
|
depositIDs []deposit.ID) (map[lntypes.Hash][]deposit.ID, error) {
|
|
|
|
// Filter the prepared mapping to only include hashes that reference
|
|
// any of the provided deposit IDs.
|
|
idSet := make(map[deposit.ID]struct{}, len(depositIDs))
|
|
for _, id := range depositIDs {
|
|
idSet[id] = struct{}{}
|
|
}
|
|
res := make(map[lntypes.Hash][]deposit.ID)
|
|
for h, ids := range s.mapIDs {
|
|
for _, id := range ids {
|
|
if _, ok := idSet[id]; ok {
|
|
res[h] = ids
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// helper to create a deposit with specific outpoint and value.
|
|
func makeDeposit(h byte, index uint32, value btcutil.Amount,
|
|
confirmationHeight int64) *deposit.Deposit {
|
|
|
|
d := &deposit.Deposit{
|
|
Value: value,
|
|
ConfirmationHeight: confirmationHeight,
|
|
}
|
|
|
|
d.Hash = chainhash.Hash{h}
|
|
d.Index = index
|
|
var id deposit.ID
|
|
id[0] = h
|
|
d.ID = id
|
|
|
|
return d
|
|
}
|
|
|
|
// helper to outpoint string as used by txin.PreviousOutPoint.String().
|
|
func outpointString(d *deposit.Deposit) string {
|
|
return wire.OutPoint{Hash: d.Hash, Index: d.Index}.String()
|
|
}
|
|
|
|
// build a sweep tx with given inputs and outputs.
|
|
func makeSweepTx(inputs []wire.OutPoint, outputs []*wire.TxOut) *wire.MsgTx {
|
|
tx := wire.NewMsgTx(2)
|
|
for _, in := range inputs {
|
|
tx.AddTxIn(&wire.TxIn{PreviousOutPoint: in})
|
|
}
|
|
for _, out := range outputs {
|
|
tx.AddTxOut(out)
|
|
}
|
|
|
|
return tx
|
|
}
|
|
|
|
// TestCheckChange exercises all relevant scenarios for checkChange.
|
|
func TestCheckChange(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
// Prepare a common change address and an alternate address.
|
|
changeAddr := &script.Parameters{PkScript: []byte{0xaa, 0xbb}}
|
|
otherAddr := &script.Parameters{PkScript: []byte{0xcc, 0xdd}}
|
|
serverAddr := &script.Parameters{PkScript: []byte{0xee, 0xff}}
|
|
|
|
// Prepare swaps (loop-ins) with varying deposit totals and selections.
|
|
// Helper to make a swap with deposits and selected amount.
|
|
makeSwap := func(h byte, deposits []*deposit.Deposit,
|
|
selected btcutil.Amount) (lntypes.Hash, *StaticAddressLoopIn) {
|
|
|
|
var hash lntypes.Hash
|
|
hash[0] = h
|
|
li := &StaticAddressLoopIn{
|
|
Deposits: deposits,
|
|
SelectedAmount: selected,
|
|
AddressParams: changeAddr,
|
|
}
|
|
return hash, li
|
|
}
|
|
|
|
// Deposits belonging to different swaps.
|
|
const confirmationHeight = 0
|
|
s1d1 := makeDeposit(1, 0, 1000, confirmationHeight)
|
|
s1d2 := makeDeposit(1, 1, 2000, confirmationHeight)
|
|
s2d1 := makeDeposit(2, 0, 1500, confirmationHeight)
|
|
s3d1 := makeDeposit(3, 0, 800, confirmationHeight)
|
|
s4d1 := makeDeposit(4, 0, 900, confirmationHeight)
|
|
|
|
// Swaps:
|
|
// A: total 3000, selected 3000 => no change.
|
|
hA, liA := makeSwap(10, []*deposit.Deposit{s1d1, s1d2}, 3000)
|
|
// B: total 1500, selected 1000 => change 500.
|
|
hB, liB := makeSwap(11, []*deposit.Deposit{s2d1}, 1000)
|
|
// C: total 800, selected 400 => change 400.
|
|
hC, liC := makeSwap(12, []*deposit.Deposit{s3d1}, 400)
|
|
// D: total 900, selected 500 => change 400.
|
|
hD, liD := makeSwap(13, []*deposit.Deposit{s4d1}, 500)
|
|
|
|
// Mapping deposits -> swaps (by deposit IDs).
|
|
mapIDs := map[lntypes.Hash][]deposit.ID{
|
|
hA: {s1d1.ID, s1d2.ID},
|
|
hB: {s2d1.ID},
|
|
hC: {s3d1.ID},
|
|
hD: {s4d1.ID},
|
|
}
|
|
|
|
loopIns := map[lntypes.Hash]*StaticAddressLoopIn{
|
|
hA: liA,
|
|
hB: liB,
|
|
hC: liC,
|
|
hD: liD,
|
|
}
|
|
|
|
// Common manager with mocked dependencies; will change inputs per test.
|
|
mgr := &Manager{
|
|
cfg: &Config{
|
|
DepositManager: &mockDepositManager{
|
|
byOutpoint: map[string]*deposit.Deposit{},
|
|
},
|
|
Store: &mockStore{
|
|
loopIns: loopIns,
|
|
mapIDs: mapIDs,
|
|
},
|
|
},
|
|
}
|
|
|
|
type testCase struct {
|
|
name string
|
|
inDeps []*deposit.Deposit // deposits referenced by tx inputs
|
|
outputs []*wire.TxOut // outputs in sweep tx
|
|
addr *script.Parameters
|
|
expectErr bool
|
|
expectedErrMsg string
|
|
}
|
|
|
|
cases := []testCase{
|
|
{
|
|
name: "no change expected (selected == total)",
|
|
inDeps: []*deposit.Deposit{s1d1, s1d2},
|
|
// No change output required.
|
|
outputs: []*wire.TxOut{
|
|
{
|
|
Value: 1337,
|
|
PkScript: serverAddr.PkScript,
|
|
},
|
|
},
|
|
addr: changeAddr,
|
|
},
|
|
{
|
|
name: "single swap change present",
|
|
inDeps: []*deposit.Deposit{s2d1}, // B -> change 500
|
|
outputs: []*wire.TxOut{
|
|
{
|
|
Value: 1337,
|
|
PkScript: serverAddr.PkScript,
|
|
},
|
|
{
|
|
Value: 500,
|
|
PkScript: changeAddr.PkScript,
|
|
},
|
|
},
|
|
addr: changeAddr,
|
|
},
|
|
{
|
|
name: "multiple swaps different change amounts",
|
|
inDeps: []*deposit.Deposit{s2d1, s3d1}, // B(500)+C(400)=900
|
|
outputs: []*wire.TxOut{
|
|
{
|
|
Value: 1337,
|
|
PkScript: serverAddr.PkScript,
|
|
},
|
|
{
|
|
Value: 900,
|
|
PkScript: changeAddr.PkScript,
|
|
},
|
|
},
|
|
addr: changeAddr,
|
|
},
|
|
{
|
|
name: "two swaps with identical change values sum correctly",
|
|
inDeps: []*deposit.Deposit{s3d1, s4d1}, // C(400)+D(400)=800
|
|
outputs: []*wire.TxOut{
|
|
{
|
|
Value: 1337,
|
|
PkScript: serverAddr.PkScript,
|
|
},
|
|
{
|
|
Value: 800,
|
|
PkScript: changeAddr.PkScript,
|
|
},
|
|
},
|
|
addr: changeAddr,
|
|
},
|
|
{
|
|
name: "missing change output results in error",
|
|
inDeps: []*deposit.Deposit{s2d1}, // expect 500
|
|
outputs: []*wire.TxOut{},
|
|
addr: changeAddr,
|
|
expectErr: true,
|
|
expectedErrMsg: "couldn't find expected change",
|
|
},
|
|
{
|
|
name: "wrong address for change output",
|
|
inDeps: []*deposit.Deposit{s2d1}, // expect 500
|
|
outputs: []*wire.TxOut{
|
|
{
|
|
Value: 1337,
|
|
PkScript: serverAddr.PkScript,
|
|
},
|
|
{
|
|
Value: 500,
|
|
PkScript: otherAddr.PkScript,
|
|
},
|
|
},
|
|
addr: changeAddr,
|
|
expectErr: true,
|
|
expectedErrMsg: "couldn't find expected change",
|
|
},
|
|
{
|
|
name: "wrong amount for change output",
|
|
inDeps: []*deposit.Deposit{s2d1}, // expect 500
|
|
outputs: []*wire.TxOut{
|
|
{
|
|
Value: 1337,
|
|
PkScript: serverAddr.PkScript,
|
|
},
|
|
{
|
|
Value: 400,
|
|
PkScript: changeAddr.PkScript,
|
|
},
|
|
},
|
|
addr: changeAddr,
|
|
expectErr: true,
|
|
expectedErrMsg: "couldn't find expected change",
|
|
},
|
|
{
|
|
name: "mixed swaps some with change some without",
|
|
inDeps: []*deposit.Deposit{s1d1, s1d2, s3d1}, // A(0)+C(400)=400
|
|
outputs: []*wire.TxOut{
|
|
{
|
|
Value: 1337,
|
|
PkScript: serverAddr.PkScript,
|
|
},
|
|
{
|
|
Value: 400,
|
|
PkScript: changeAddr.PkScript,
|
|
},
|
|
{
|
|
Value: 1000,
|
|
PkScript: otherAddr.PkScript,
|
|
},
|
|
},
|
|
addr: changeAddr,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Prepare inputs mapping for deposit manager.
|
|
mdm := &mockDepositManager{
|
|
byOutpoint: map[string]*deposit.Deposit{},
|
|
}
|
|
inputs := make([]wire.OutPoint, 0, len(tc.inDeps))
|
|
for _, d := range tc.inDeps {
|
|
mdm.byOutpoint[outpointString(d)] = d
|
|
inputs = append(
|
|
inputs, wire.OutPoint{
|
|
Hash: d.Hash,
|
|
Index: d.Index,
|
|
},
|
|
)
|
|
}
|
|
mgr.cfg.DepositManager = mdm
|
|
|
|
tx := makeSweepTx(inputs, tc.outputs)
|
|
err := mgr.checkChange(ctx, tx, tc.addr)
|
|
if tc.expectErr {
|
|
require.Error(t, err)
|
|
if tc.expectedErrMsg != "" {
|
|
require.ErrorContains(
|
|
t, err, tc.expectedErrMsg,
|
|
)
|
|
}
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|