mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr: refresh deposits before spend selection
Refresh the active static-address deposit set against lnd's wallet view before quote, loop-in, withdrawal, channel-open, and autoloop selection paths. This prevents stale persisted Deposited records from being selected after replacement, reorg, or an external spend.
This commit is contained in:
parent
1c89ff83f1
commit
dc7da41b28
13 changed files with 112 additions and 15 deletions
|
|
@ -946,6 +946,12 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
|
||||||
// number of deposits to quote for.
|
// number of deposits to quote for.
|
||||||
numDeposits := 0
|
numDeposits := 0
|
||||||
if autoSelectDeposits {
|
if autoSelectDeposits {
|
||||||
|
err = s.depositManager.EnsureDepositsFresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to refresh deposits: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
deposits, err := s.depositManager.GetActiveDepositsInState(
|
deposits, err := s.depositManager.GetActiveDepositsInState(
|
||||||
deposit.Deposited,
|
deposit.Deposited,
|
||||||
)
|
)
|
||||||
|
|
@ -980,6 +986,12 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
|
||||||
|
|
||||||
numDeposits = len(selectedDeposits)
|
numDeposits = len(selectedDeposits)
|
||||||
} else if len(req.DepositOutpoints) > 0 {
|
} else if len(req.DepositOutpoints) > 0 {
|
||||||
|
err = s.depositManager.EnsureDepositsFresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to refresh deposits: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
// If deposits are selected, we need to retrieve them to
|
// If deposits are selected, we need to retrieve them to
|
||||||
// calculate the total value which we request a quote for.
|
// calculate the total value which we request a quote for.
|
||||||
depositList, err := s.ListStaticAddressDeposits(
|
depositList, err := s.ListStaticAddressDeposits(
|
||||||
|
|
@ -1798,6 +1810,12 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context,
|
||||||
return nil, fmt.Errorf("must select either all or some utxos")
|
return nil, fmt.Errorf("must select either all or some utxos")
|
||||||
|
|
||||||
case isAllSelected:
|
case isAllSelected:
|
||||||
|
err = s.depositManager.EnsureDepositsFresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to refresh deposits: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
deposits, err := s.depositManager.GetActiveDepositsInState(
|
deposits, err := s.depositManager.GetActiveDepositsInState(
|
||||||
deposit.Deposited,
|
deposit.Deposited,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
|
@ -13,6 +14,7 @@ import (
|
||||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||||
"github.com/lightninglabs/loop/staticaddr/script"
|
"github.com/lightninglabs/loop/staticaddr/script"
|
||||||
mock_lnd "github.com/lightninglabs/loop/test"
|
mock_lnd "github.com/lightninglabs/loop/test"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -60,6 +62,33 @@ func (s *staticAddrDepositStore) AllDeposits(context.Context) (
|
||||||
return s.allDeposits, nil
|
return s.allDeposits, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type staticAddrTestAddressManager struct{}
|
||||||
|
|
||||||
|
func (s *staticAddrTestAddressManager) GetStaticAddressParameters(
|
||||||
|
context.Context) (*script.Parameters, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *staticAddrTestAddressManager) GetStaticAddress(
|
||||||
|
context.Context) (*script.StaticAddress, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *staticAddrTestAddressManager) ListUnspent(context.Context,
|
||||||
|
int32, int32) ([]*lnwallet.Utxo, error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *staticAddrTestAddressManager) GetTaprootAddress(
|
||||||
|
*btcec.PublicKey, *btcec.PublicKey, int64) (*btcutil.AddressTaproot,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// newTestDepositManager creates a deposit manager backed by seeded deposits.
|
// newTestDepositManager creates a deposit manager backed by seeded deposits.
|
||||||
func newTestDepositManager(
|
func newTestDepositManager(
|
||||||
deposits ...*deposit.Deposit) *deposit.Manager {
|
deposits ...*deposit.Deposit) *deposit.Manager {
|
||||||
|
|
@ -70,6 +99,7 @@ func newTestDepositManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
return deposit.NewManager(&deposit.ManagerConfig{
|
return deposit.NewManager(&deposit.ManagerConfig{
|
||||||
|
AddressManager: &staticAddrTestAddressManager{},
|
||||||
Store: &staticAddrDepositStore{
|
Store: &staticAddrDepositStore{
|
||||||
allDeposits: deposits,
|
allDeposits: deposits,
|
||||||
byOutpoint: byOutpoint,
|
byOutpoint: byOutpoint,
|
||||||
|
|
|
||||||
|
|
@ -784,6 +784,11 @@ func (m *mockAddressManager) GetStaticAddress(_ context.Context) (
|
||||||
// noopDepositManager is a stub DepositManager used to satisfy FSM config.
|
// noopDepositManager is a stub DepositManager used to satisfy FSM config.
|
||||||
type noopDepositManager struct{}
|
type noopDepositManager struct{}
|
||||||
|
|
||||||
|
// EnsureDepositsFresh implements DepositManager with a no-op.
|
||||||
|
func (n *noopDepositManager) EnsureDepositsFresh(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllDeposits implements DepositManager with a no-op.
|
// GetAllDeposits implements DepositManager with a no-op.
|
||||||
func (n *noopDepositManager) GetAllDeposits(_ context.Context) (
|
func (n *noopDepositManager) GetAllDeposits(_ context.Context) (
|
||||||
[]*deposit.Deposit, error) {
|
[]*deposit.Deposit, error) {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ func (m *Manager) PrepareAutoloopLoopIn(ctx context.Context,
|
||||||
return nil, 0, false, ErrNoAutoloopCandidate
|
return nil, 0, false, ErrNoAutoloopCandidate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := m.cfg.DepositManager.EnsureDepositsFresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
allDeposits, err := m.cfg.DepositManager.GetActiveDepositsInState(
|
allDeposits, err := m.cfg.DepositManager.GetActiveDepositsInState(
|
||||||
deposit.Deposited,
|
deposit.Deposited,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -247,8 +247,7 @@ func filterAutoloopCandidateDeposits(maxAmount btcutil.Amount,
|
||||||
}
|
}
|
||||||
|
|
||||||
residualLife := int64(blocksUntilDepositExpiry(
|
residualLife := int64(blocksUntilDepositExpiry(
|
||||||
uint32(confirmationHeight),
|
uint32(confirmationHeight), blockHeight, csvExpiry,
|
||||||
blockHeight, csvExpiry,
|
|
||||||
))
|
))
|
||||||
|
|
||||||
eligibleDeposits = append(
|
eligibleDeposits = append(
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,9 @@ type AddressManager interface {
|
||||||
|
|
||||||
// DepositManager handles the interaction of loop-ins with deposits.
|
// DepositManager handles the interaction of loop-ins with deposits.
|
||||||
type DepositManager interface {
|
type DepositManager interface {
|
||||||
|
// EnsureDepositsFresh reconciles active deposits with the wallet view.
|
||||||
|
EnsureDepositsFresh(ctx context.Context) error
|
||||||
|
|
||||||
// GetAllDeposits returns all known deposits from the database store.
|
// GetAllDeposits returns all known deposits from the database store.
|
||||||
GetAllDeposits(ctx context.Context) ([]*deposit.Deposit, error)
|
GetAllDeposits(ctx context.Context) ([]*deposit.Deposit, error)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -630,6 +630,11 @@ func (m *Manager) initiateLoopIn(ctx context.Context,
|
||||||
selectedDeposits []*deposit.Deposit
|
selectedDeposits []*deposit.Deposit
|
||||||
)
|
)
|
||||||
|
|
||||||
|
err = m.cfg.DepositManager.EnsureDepositsFresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to refresh deposits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Determine which deposits to use for the loop-in swap. If none are
|
// Determine which deposits to use for the loop-in swap. If none are
|
||||||
// selected by the client, we will coin-select them based on the amount.
|
// selected by the client, we will coin-select them based on the amount.
|
||||||
switch {
|
switch {
|
||||||
|
|
@ -876,8 +881,8 @@ func SelectDeposits(targetAmount btcutil.Amount,
|
||||||
// prefers deposits the server can accept immediately. Within each group
|
// prefers deposits the server can accept immediately. Within each group
|
||||||
// we prefer larger deposits, then earlier expiries.
|
// we prefer larger deposits, then earlier expiries.
|
||||||
sort.Slice(deposits, func(i, j int) bool {
|
sort.Slice(deposits, func(i, j int) bool {
|
||||||
iConfirmationHeight := uint32(deposits[i].GetConfirmationHeight())
|
iConfirmationHeight := deposits[i].GetConfirmationHeight()
|
||||||
jConfirmationHeight := uint32(deposits[j].GetConfirmationHeight())
|
jConfirmationHeight := deposits[j].GetConfirmationHeight()
|
||||||
iConfirmed := iConfirmationHeight > 0
|
iConfirmed := iConfirmationHeight > 0
|
||||||
jConfirmed := jConfirmationHeight > 0
|
jConfirmed := jConfirmationHeight > 0
|
||||||
if iConfirmed != jConfirmed {
|
if iConfirmed != jConfirmed {
|
||||||
|
|
@ -886,10 +891,12 @@ func SelectDeposits(targetAmount btcutil.Amount,
|
||||||
|
|
||||||
if deposits[i].Value == deposits[j].Value {
|
if deposits[i].Value == deposits[j].Value {
|
||||||
iExp := blocksUntilDepositExpiry(
|
iExp := blocksUntilDepositExpiry(
|
||||||
iConfirmationHeight, blockHeight, csvExpiry,
|
uint32(iConfirmationHeight), blockHeight,
|
||||||
|
csvExpiry,
|
||||||
)
|
)
|
||||||
jExp := blocksUntilDepositExpiry(
|
jExp := blocksUntilDepositExpiry(
|
||||||
jConfirmationHeight, blockHeight, csvExpiry,
|
uint32(jConfirmationHeight), blockHeight,
|
||||||
|
csvExpiry,
|
||||||
)
|
)
|
||||||
|
|
||||||
return iExp < jExp
|
return iExp < jExp
|
||||||
|
|
|
||||||
|
|
@ -393,6 +393,10 @@ type mockDepositManager struct {
|
||||||
byOutpoint map[string]*deposit.Deposit
|
byOutpoint map[string]*deposit.Deposit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockDepositManager) EnsureDepositsFresh(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockDepositManager) GetAllDeposits(_ context.Context) (
|
func (m *mockDepositManager) GetAllDeposits(_ context.Context) (
|
||||||
[]*deposit.Deposit, error) {
|
[]*deposit.Deposit, error) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type DepositManager interface {
|
type DepositManager interface {
|
||||||
|
// EnsureDepositsFresh reconciles active deposits with the wallet view.
|
||||||
|
EnsureDepositsFresh(ctx context.Context) error
|
||||||
|
|
||||||
// AllOutpointsActiveDeposits returns all deposits that are in the
|
// AllOutpointsActiveDeposits returns all deposits that are in the
|
||||||
// given state. If the state filter is fsm.StateTypeNone, all deposits
|
// given state. If the state filter is fsm.StateTypeNone, all deposits
|
||||||
// are returned.
|
// are returned.
|
||||||
|
|
|
||||||
|
|
@ -267,11 +267,6 @@ func (m *Manager) OpenChannel(ctx context.Context,
|
||||||
).FeePerKWeight()
|
).FeePerKWeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
// There are three ways in which we select deposits to open a channel
|
|
||||||
// with. 1.) The user manually selects the deposits. 2.) The user only
|
|
||||||
// selects a local channel amount in which case we coin-select deposits
|
|
||||||
// to cover for it. 3.) The user selects the fundmax flag, in which case
|
|
||||||
// we select all deposits to fund the channel.
|
|
||||||
if len(req.Outpoints) > 0 {
|
if len(req.Outpoints) > 0 {
|
||||||
// Ensure that the deposits are in a state in which they are
|
// Ensure that the deposits are in a state in which they are
|
||||||
// available for a channel open.
|
// available for a channel open.
|
||||||
|
|
@ -288,6 +283,12 @@ func (m *Manager) OpenChannel(ctx context.Context,
|
||||||
return nil, fmt.Errorf("%w in request", err)
|
return nil, fmt.Errorf("%w in request", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = m.cfg.DepositManager.EnsureDepositsFresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to refresh deposits: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
deposits, allActive =
|
deposits, allActive =
|
||||||
m.cfg.DepositManager.AllOutpointsActiveDeposits(
|
m.cfg.DepositManager.AllOutpointsActiveDeposits(
|
||||||
outpoints, deposit.Deposited,
|
outpoints, deposit.Deposited,
|
||||||
|
|
@ -296,6 +297,12 @@ func (m *Manager) OpenChannel(ctx context.Context,
|
||||||
return nil, ErrOpeningChannelUnavailableDeposits
|
return nil, ErrOpeningChannelUnavailableDeposits
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
err = m.cfg.DepositManager.EnsureDepositsFresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to refresh deposits: %w",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
|
||||||
// We have to select the deposits that are used to fund the
|
// We have to select the deposits that are used to fund the
|
||||||
// channel.
|
// channel.
|
||||||
deposits, err = m.cfg.DepositManager.GetActiveDepositsInState(
|
deposits, err = m.cfg.DepositManager.GetActiveDepositsInState(
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ type mockDepositManager struct {
|
||||||
calls []transitionCall
|
calls []transitionCall
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *mockDepositManager) EnsureDepositsFresh(context.Context) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *mockDepositManager) AllOutpointsActiveDeposits([]wire.OutPoint,
|
func (m *mockDepositManager) AllOutpointsActiveDeposits([]wire.OutPoint,
|
||||||
fsm.StateType) ([]*deposit.Deposit, bool) {
|
fsm.StateType) ([]*deposit.Deposit, bool) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,24 @@ type AddressManager interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type DepositManager interface {
|
type DepositManager interface {
|
||||||
|
// EnsureDepositsFresh reconciles active deposits with the wallet view.
|
||||||
|
EnsureDepositsFresh(ctx context.Context) error
|
||||||
|
|
||||||
|
// GetActiveDepositsInState returns all active deposits in the given
|
||||||
|
// state.
|
||||||
GetActiveDepositsInState(stateFilter fsm.StateType) ([]*deposit.Deposit,
|
GetActiveDepositsInState(stateFilter fsm.StateType) ([]*deposit.Deposit,
|
||||||
error)
|
error)
|
||||||
|
|
||||||
|
// AllOutpointsActiveDeposits returns all active deposits referenced by
|
||||||
|
// the outpoints if every deposit is active and in the given state.
|
||||||
AllOutpointsActiveDeposits(outpoints []wire.OutPoint,
|
AllOutpointsActiveDeposits(outpoints []wire.OutPoint,
|
||||||
stateFilter fsm.StateType) ([]*deposit.Deposit, bool)
|
stateFilter fsm.StateType) ([]*deposit.Deposit, bool)
|
||||||
|
|
||||||
|
// TransitionDeposits transitions the deposits with the given event and
|
||||||
|
// waits until they reach the expected final state.
|
||||||
TransitionDeposits(ctx context.Context, deposits []*deposit.Deposit,
|
TransitionDeposits(ctx context.Context, deposits []*deposit.Deposit,
|
||||||
event fsm.EventType, expectedFinalState fsm.StateType) error
|
event fsm.EventType, expectedFinalState fsm.StateType) error
|
||||||
|
|
||||||
|
// UpdateDeposit persists the current deposit fields.
|
||||||
UpdateDeposit(ctx context.Context, d *deposit.Deposit) error
|
UpdateDeposit(ctx context.Context, d *deposit.Deposit) error
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -320,6 +320,11 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
|
||||||
allWithdrawing bool
|
allWithdrawing bool
|
||||||
)
|
)
|
||||||
|
|
||||||
|
err := m.cfg.DepositManager.EnsureDepositsFresh(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("unable to refresh deposits: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure that the deposits are in a state in which they can be
|
// Ensure that the deposits are in a state in which they can be
|
||||||
// withdrawn.
|
// withdrawn.
|
||||||
deposits, allDeposited = m.cfg.DepositManager.AllOutpointsActiveDeposits(
|
deposits, allDeposited = m.cfg.DepositManager.AllOutpointsActiveDeposits(
|
||||||
|
|
@ -390,10 +395,7 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var withdrawalAddress btcutil.Address
|
||||||
withdrawalAddress btcutil.Address
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if the user provided an address to withdraw to. If not, we'll
|
// Check if the user provided an address to withdraw to. If not, we'll
|
||||||
// generate a new address for them.
|
// generate a new address for them.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue