2024-05-06 14:19:07 +02:00
|
|
|
package withdraw
|
|
|
|
|
|
|
|
|
|
import (
|
2025-11-17 14:30:49 +01:00
|
|
|
"bytes"
|
2024-05-06 14:19:07 +02:00
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
2025-03-19 13:36:49 +01:00
|
|
|
"sync"
|
2025-03-10 19:36:54 -03:00
|
|
|
"sync/atomic"
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2024-12-03 13:44:04 +01:00
|
|
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
2024-05-06 14:19:07 +02:00
|
|
|
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
|
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
2025-11-17 14:30:49 +01:00
|
|
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
2024-05-06 14:19:07 +02:00
|
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
|
|
|
"github.com/btcsuite/btcd/txscript"
|
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
2025-03-27 15:58:51 +01:00
|
|
|
"github.com/btcsuite/btcwallet/chain"
|
2024-05-06 14:19:07 +02:00
|
|
|
"github.com/lightninglabs/lndclient"
|
|
|
|
|
"github.com/lightninglabs/loop/staticaddr/deposit"
|
2025-11-17 14:30:49 +01:00
|
|
|
"github.com/lightninglabs/loop/staticaddr/staticutil"
|
2024-05-06 14:19:07 +02:00
|
|
|
staticaddressrpc "github.com/lightninglabs/loop/swapserverrpc"
|
2025-11-17 14:30:49 +01:00
|
|
|
"github.com/lightningnetwork/lnd/funding"
|
2024-05-06 14:19:07 +02:00
|
|
|
"github.com/lightningnetwork/lnd/input"
|
2025-11-17 14:30:49 +01:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc"
|
2024-05-06 14:19:07 +02:00
|
|
|
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
|
|
|
|
"github.com/lightningnetwork/lnd/lntypes"
|
|
|
|
|
"github.com/lightningnetwork/lnd/lnwallet"
|
|
|
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
2025-06-23 00:56:58 -03:00
|
|
|
"golang.org/x/sync/errgroup"
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
2025-03-19 13:36:49 +01:00
|
|
|
// ErrWithdrawingMixedDeposits is returned when a withdrawal is
|
|
|
|
|
// requested for deposits in different states.
|
|
|
|
|
ErrWithdrawingMixedDeposits = errors.New("need to withdraw deposits " +
|
|
|
|
|
"having the same state, either all deposited or all " +
|
|
|
|
|
"withdrawing")
|
|
|
|
|
|
|
|
|
|
// ErrDiffPreviousWithdrawalTx signals that the user selected new
|
|
|
|
|
// deposits that have different previous withdrawal transactions.
|
|
|
|
|
ErrDiffPreviousWithdrawalTx = errors.New("can't bump fee for " +
|
|
|
|
|
"deposits with different previous withdrawal tx hash")
|
|
|
|
|
|
|
|
|
|
// ErrMissingPreviousWithdrawn is returned when the user tries to bump
|
|
|
|
|
// the fee for a subset of previously selected deposits to withdraw.
|
|
|
|
|
ErrMissingPreviousWithdrawn = errors.New("can't bump fee for subset " +
|
|
|
|
|
"of clustered deposits")
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-09-08 17:22:45 +02:00
|
|
|
// ErrMissingFinalizedTx is returned if previously withdrawn deposits
|
|
|
|
|
// don't have a finalized withdrawal tx attached.
|
|
|
|
|
ErrMissingFinalizedTx = errors.New("deposit does not have a " +
|
|
|
|
|
"finalized withdrawal tx, can't bump fee")
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
// MinConfs is the minimum number of confirmations we require for a
|
|
|
|
|
// deposit to be considered withdrawn.
|
|
|
|
|
MinConfs int32 = 3
|
|
|
|
|
|
|
|
|
|
// Is the default confirmation target for the fee estimation of the
|
|
|
|
|
// withdrawal transaction.
|
|
|
|
|
defaultConfTarget int32 = 3
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// ManagerConfig holds the configuration for the address manager.
|
|
|
|
|
type ManagerConfig struct {
|
|
|
|
|
// StaticAddressServerClient is the client that calls the swap server
|
|
|
|
|
// rpcs to negotiate static address withdrawals.
|
|
|
|
|
StaticAddressServerClient staticaddressrpc.StaticAddressServerClient
|
|
|
|
|
|
|
|
|
|
// AddressManager gives the withdrawal manager access to static address
|
|
|
|
|
// parameters.
|
|
|
|
|
AddressManager AddressManager
|
|
|
|
|
|
|
|
|
|
// DepositManager gives the withdrawal manager access to the deposits
|
|
|
|
|
// enabling it to create and manage withdrawals.
|
|
|
|
|
DepositManager DepositManager
|
|
|
|
|
|
|
|
|
|
// WalletKit is the wallet client that is used to derive new keys from
|
|
|
|
|
// lnd's wallet.
|
|
|
|
|
WalletKit lndclient.WalletKitClient
|
|
|
|
|
|
|
|
|
|
// ChainParams is the chain configuration(mainnet, testnet...) this
|
|
|
|
|
// manager uses.
|
|
|
|
|
ChainParams *chaincfg.Params
|
|
|
|
|
|
|
|
|
|
// ChainNotifier is the chain notifier that is used to listen for new
|
|
|
|
|
// blocks.
|
|
|
|
|
ChainNotifier lndclient.ChainNotifierClient
|
|
|
|
|
|
|
|
|
|
// Signer is the signer client that is used to sign transactions.
|
|
|
|
|
Signer lndclient.SignerClient
|
2025-06-02 15:49:03 +02:00
|
|
|
|
|
|
|
|
// Store is the store that is used to persist the finalized withdrawal
|
|
|
|
|
// transactions.
|
|
|
|
|
Store *SqlStore
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newWithdrawalRequest is used to send withdrawal request to the manager main
|
|
|
|
|
// loop.
|
|
|
|
|
type newWithdrawalRequest struct {
|
2024-11-29 14:47:51 +01:00
|
|
|
outpoints []wire.OutPoint
|
|
|
|
|
respChan chan *newWithdrawalResponse
|
|
|
|
|
destAddr string
|
|
|
|
|
satPerVbyte int64
|
2024-12-03 13:44:04 +01:00
|
|
|
amount int64
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// newWithdrawalResponse is used to return withdrawal info and error to the
|
|
|
|
|
// server.
|
|
|
|
|
type newWithdrawalResponse struct {
|
2025-04-23 21:33:08 -03:00
|
|
|
txHash string
|
|
|
|
|
withdrawalAddress string
|
|
|
|
|
err error
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Manager manages the withdrawal state machines.
|
|
|
|
|
type Manager struct {
|
|
|
|
|
cfg *ManagerConfig
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
// mu protects access to finalizedWithdrawalTxns.
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
// newWithdrawalRequestChan receives a list of outpoints that should be
|
|
|
|
|
// withdrawn. The request is forwarded to the managers main loop.
|
|
|
|
|
newWithdrawalRequestChan chan newWithdrawalRequest
|
|
|
|
|
|
|
|
|
|
// exitChan signals subroutines that the withdrawal manager is exiting.
|
|
|
|
|
exitChan chan struct{}
|
|
|
|
|
|
|
|
|
|
// initiationHeight stores the currently best known block height.
|
2025-03-10 19:36:54 -03:00
|
|
|
initiationHeight atomic.Uint32
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2026-02-28 10:29:25 +01:00
|
|
|
// finalizedWithdrawalTxns are the finalized withdrawal transactions
|
|
|
|
|
// that are published to the network and re-published on block arrivals.
|
2024-05-06 14:19:07 +02:00
|
|
|
finalizedWithdrawalTxns map[chainhash.Hash]*wire.MsgTx
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewManager creates a new deposit withdrawal manager.
|
2025-11-16 00:08:19 -03:00
|
|
|
func NewManager(cfg *ManagerConfig, currentHeight uint32) (*Manager, error) {
|
|
|
|
|
if currentHeight == 0 {
|
|
|
|
|
return nil, fmt.Errorf("invalid current height %d",
|
|
|
|
|
currentHeight)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-14 14:16:07 -03:00
|
|
|
m := &Manager{
|
2024-05-06 14:19:07 +02:00
|
|
|
cfg: cfg,
|
|
|
|
|
finalizedWithdrawalTxns: make(map[chainhash.Hash]*wire.MsgTx),
|
|
|
|
|
exitChan: make(chan struct{}),
|
|
|
|
|
newWithdrawalRequestChan: make(chan newWithdrawalRequest),
|
|
|
|
|
}
|
2025-03-14 14:16:07 -03:00
|
|
|
m.initiationHeight.Store(currentHeight)
|
|
|
|
|
|
2025-11-16 00:08:19 -03:00
|
|
|
return m, nil
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Run runs the deposit withdrawal manager.
|
2025-06-12 22:00:31 +02:00
|
|
|
func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error {
|
2024-05-06 14:19:07 +02:00
|
|
|
newBlockChan, newBlockErrChan, err :=
|
|
|
|
|
m.cfg.ChainNotifier.RegisterBlockEpochNtfn(ctx)
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2025-08-26 09:23:20 +02:00
|
|
|
log.Errorf("unable to register for block epoch "+
|
|
|
|
|
"notifications: %v", err)
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = m.recoverWithdrawals(ctx)
|
|
|
|
|
if err != nil {
|
2025-08-26 09:23:20 +02:00
|
|
|
log.Errorf("unable to recover withdrawals: %v", err)
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Communicate to the caller that the address manager has completed its
|
|
|
|
|
// initialization.
|
2025-06-12 22:00:31 +02:00
|
|
|
close(initChan)
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-newBlockChan:
|
|
|
|
|
err = m.republishWithdrawals(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error republishing withdrawals: %v",
|
|
|
|
|
err)
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-03 13:44:04 +01:00
|
|
|
case req := <-m.newWithdrawalRequestChan:
|
2025-04-23 21:33:08 -03:00
|
|
|
txHash, withdrawalAddress, err := m.WithdrawDeposits(
|
2024-12-03 13:44:04 +01:00
|
|
|
ctx, req.outpoints, req.destAddr,
|
|
|
|
|
req.satPerVbyte, req.amount,
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error withdrawing deposits: %v",
|
|
|
|
|
err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We forward the initialized loop-in and error to
|
|
|
|
|
// DeliverLoopInRequest.
|
|
|
|
|
resp := &newWithdrawalResponse{
|
2025-04-23 21:33:08 -03:00
|
|
|
txHash: txHash,
|
|
|
|
|
withdrawalAddress: withdrawalAddress,
|
|
|
|
|
err: err,
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
select {
|
2024-12-03 13:44:04 +01:00
|
|
|
case req.respChan <- resp:
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
// Notify subroutines that the main loop has
|
|
|
|
|
// been canceled.
|
|
|
|
|
close(m.exitChan)
|
|
|
|
|
|
|
|
|
|
return ctx.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case err = <-newBlockErrChan:
|
|
|
|
|
return err
|
|
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
// Signal subroutines that the manager is exiting.
|
|
|
|
|
close(m.exitChan)
|
|
|
|
|
|
|
|
|
|
return ctx.Err()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) recoverWithdrawals(ctx context.Context) error {
|
2025-07-02 15:05:51 +02:00
|
|
|
// To recover withdrawals we cluster those with equal withdrawal
|
|
|
|
|
// addresses and publish their withdrawal tx. Each cluster represents a
|
|
|
|
|
// separate withdrawal intent by the user.
|
|
|
|
|
withdrawingDeposits, err := m.cfg.DepositManager.GetActiveDepositsInState(
|
|
|
|
|
deposit.Withdrawing,
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Group the deposits by their finalized withdrawal transaction.
|
2025-06-23 09:36:29 -03:00
|
|
|
depositsByWithdrawalTx := make(map[chainhash.Hash][]*deposit.Deposit)
|
|
|
|
|
hash2tx := make(map[chainhash.Hash]*wire.MsgTx)
|
2025-07-02 15:05:51 +02:00
|
|
|
for _, d := range withdrawingDeposits {
|
2024-05-06 14:19:07 +02:00
|
|
|
withdrawalTx := d.FinalizedWithdrawalTx
|
|
|
|
|
if withdrawalTx == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2025-06-23 09:36:29 -03:00
|
|
|
txid := withdrawalTx.TxHash()
|
|
|
|
|
hash2tx[txid] = withdrawalTx
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-06-23 09:36:29 -03:00
|
|
|
depositsByWithdrawalTx[txid] = append(
|
|
|
|
|
depositsByWithdrawalTx[txid], d,
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 00:56:58 -03:00
|
|
|
// Publishing a transaction can take a while in neutrino mode, so
|
|
|
|
|
// do it in parallel.
|
|
|
|
|
eg := &errgroup.Group{}
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
// We can now reinstate each cluster of deposits for a withdrawal.
|
2025-06-23 09:36:29 -03:00
|
|
|
for txid, deposits := range depositsByWithdrawalTx {
|
2025-06-23 00:56:58 -03:00
|
|
|
eg.Go(func() error {
|
|
|
|
|
err := m.cfg.DepositManager.TransitionDeposits(
|
|
|
|
|
ctx, deposits, deposit.OnWithdrawInitiated,
|
|
|
|
|
deposit.Withdrawing,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-06-23 09:36:29 -03:00
|
|
|
tx, ok := hash2tx[txid]
|
|
|
|
|
if !ok {
|
|
|
|
|
return fmt.Errorf("can't find tx %v", txid)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-23 00:56:58 -03:00
|
|
|
_, err = m.publishFinalizedWithdrawalTx(ctx, tx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-06-23 00:56:58 -03:00
|
|
|
err = m.handleWithdrawal(
|
|
|
|
|
ctx, deposits, tx.TxHash(),
|
|
|
|
|
tx.TxOut[0].PkScript,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
m.finalizedWithdrawalTxns[tx.TxHash()] = tx
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-06-23 00:56:58 -03:00
|
|
|
// Wait for all goroutines to report back.
|
|
|
|
|
if err := eg.Wait(); err != nil {
|
|
|
|
|
return fmt.Errorf("error recovering withdrawals: %w", err)
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-03 13:44:04 +01:00
|
|
|
// WithdrawDeposits starts a deposits withdrawal flow. If the amount is set to 0
|
|
|
|
|
// the full amount of the selected deposits will be withdrawn.
|
2024-05-06 14:19:07 +02:00
|
|
|
func (m *Manager) WithdrawDeposits(ctx context.Context,
|
2024-12-03 13:44:04 +01:00
|
|
|
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
|
|
|
|
|
amount int64) (string, string, error) {
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
if len(outpoints) == 0 {
|
|
|
|
|
return "", "", fmt.Errorf("no outpoints selected to " +
|
|
|
|
|
"withdraw, unconfirmed deposits can't be withdrawn")
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
var (
|
|
|
|
|
deposits []*deposit.Deposit
|
|
|
|
|
allDeposited bool
|
|
|
|
|
allWithdrawing bool
|
|
|
|
|
)
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
// Ensure that the deposits are in a state in which they can be
|
|
|
|
|
// withdrawn.
|
2025-03-19 13:36:49 +01:00
|
|
|
deposits, allDeposited = m.cfg.DepositManager.AllOutpointsActiveDeposits(
|
2024-12-03 13:44:04 +01:00
|
|
|
outpoints, deposit.Deposited,
|
|
|
|
|
)
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
// If not all passed outpoints are in state Deposited, we'll check if
|
|
|
|
|
// they are all in state Withdrawing. If they are, then the user is
|
2025-11-17 14:30:49 +01:00
|
|
|
// requesting a fee bump, if not, we'll return an error as we only allow
|
2025-03-19 13:36:49 +01:00
|
|
|
// fee bumping deposits in state Withdrawing.
|
|
|
|
|
if !allDeposited {
|
|
|
|
|
deposits, allWithdrawing = m.cfg.DepositManager.AllOutpointsActiveDeposits(
|
|
|
|
|
outpoints, deposit.Withdrawing,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if !allWithdrawing {
|
|
|
|
|
return "", "", ErrWithdrawingMixedDeposits
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-08 17:22:45 +02:00
|
|
|
// Ensure that all previously withdrawn deposits reference their
|
|
|
|
|
// finalized withdrawal tx.
|
|
|
|
|
for _, d := range deposits {
|
|
|
|
|
if d.FinalizedWithdrawalTx == nil {
|
|
|
|
|
return "", "", ErrMissingFinalizedTx
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
// If republishing of an existing withdrawal is requested we
|
|
|
|
|
// ensure that all deposits remain clustered in the context of
|
|
|
|
|
// the same withdrawal tx. We do this by checking that they have
|
|
|
|
|
// the same previous withdrawal tx hash. This ensures that the
|
|
|
|
|
// shape of the transaction stays the same.
|
|
|
|
|
prevWithdrawalTx := deposits[0].FinalizedWithdrawalTx
|
|
|
|
|
hash := prevWithdrawalTx.TxHash()
|
|
|
|
|
for i := 1; i < len(deposits); i++ {
|
|
|
|
|
if deposits[i].FinalizedWithdrawalTx.TxHash() != hash {
|
|
|
|
|
return "", "", ErrDiffPreviousWithdrawalTx
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We also avoid that the user selects a subset of previously
|
|
|
|
|
// clustered deposits for a fee bump. This would result in a
|
|
|
|
|
// different transaction shape.
|
|
|
|
|
outpointMap := make(map[wire.OutPoint]struct{})
|
|
|
|
|
for _, d := range deposits {
|
|
|
|
|
outpointMap[d.OutPoint] = struct{}{}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check that all previously withdrawn deposits are included in
|
|
|
|
|
// the new withdrawal.
|
|
|
|
|
for _, in := range prevWithdrawalTx.TxIn {
|
|
|
|
|
if _, ok := outpointMap[in.PreviousOutPoint]; !ok {
|
|
|
|
|
return "", "", ErrMissingPreviousWithdrawn
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(deposits) != len(prevWithdrawalTx.TxIn) {
|
|
|
|
|
return "", "", ErrMissingPreviousWithdrawn
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2024-11-29 14:47:51 +01:00
|
|
|
var (
|
|
|
|
|
withdrawalAddress btcutil.Address
|
|
|
|
|
err error
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
2024-11-29 14:47:51 +01:00
|
|
|
|
|
|
|
|
// Check if the user provided an address to withdraw to. If not, we'll
|
|
|
|
|
// generate a new address for them.
|
|
|
|
|
if destAddr != "" {
|
|
|
|
|
withdrawalAddress, err = btcutil.DecodeAddress(
|
|
|
|
|
destAddr, m.cfg.ChainParams,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", err
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
withdrawalAddress, err = m.cfg.WalletKit.NextAddr(
|
|
|
|
|
ctx, lnwallet.DefaultAccountName,
|
|
|
|
|
walletrpc.AddressType_TAPROOT_PUBKEY, false,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", err
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-05-21 14:17:14 +02:00
|
|
|
var withdrawFeeRate chainfee.SatPerKWeight
|
|
|
|
|
if satPerVbyte == 0 {
|
|
|
|
|
withdrawFeeRate, err = m.cfg.WalletKit.EstimateFeeRate(
|
|
|
|
|
ctx, defaultConfTarget,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", fmt.Errorf("error estimating fee "+
|
|
|
|
|
"rate: %w", err)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
withdrawFeeRate = chainfee.SatPerKVByte(
|
|
|
|
|
satPerVbyte * 1000,
|
|
|
|
|
).FeePerKWeight()
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
finalizedTx, _, err := m.CreateFinalizedWithdrawalTx(
|
2025-05-21 14:17:14 +02:00
|
|
|
ctx, deposits, withdrawalAddress, withdrawFeeRate, amount,
|
|
|
|
|
lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE,
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", err
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
published, err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !published {
|
|
|
|
|
return "", "", nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
withdrawalPkScript, err := txscript.PayToAddrScript(withdrawalAddress)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", fmt.Errorf("could not get withdrawal "+
|
|
|
|
|
"pkscript: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If this is the first time this cluster of deposits is withdrawn, we
|
|
|
|
|
// start a goroutine that listens for the spent of the first input of
|
|
|
|
|
// the withdrawal transaction.
|
|
|
|
|
// Since we ensure above that the same ensemble of deposits is
|
|
|
|
|
// republished in case of a fee bump, it suffices if only one spent
|
|
|
|
|
// notifier is run.
|
|
|
|
|
if allDeposited {
|
2025-06-02 15:49:03 +02:00
|
|
|
// Persist info about the finalized withdrawal.
|
|
|
|
|
err = m.cfg.Store.CreateWithdrawal(ctx, deposits)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error persisting "+
|
|
|
|
|
"withdrawal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
err = m.handleWithdrawal(
|
|
|
|
|
ctx, deposits, finalizedTx.TxHash(), withdrawalPkScript,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If a previous withdrawal existed across the selected deposits, and
|
|
|
|
|
// it isn't the same as the new withdrawal, we remove it from the
|
|
|
|
|
// finalized withdrawals to stop republishing it on block arrivals.
|
|
|
|
|
deposits[0].Lock()
|
|
|
|
|
prevTx := deposits[0].FinalizedWithdrawalTx
|
|
|
|
|
deposits[0].Unlock()
|
|
|
|
|
|
|
|
|
|
if prevTx != nil && prevTx.TxHash() != finalizedTx.TxHash() {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
delete(m.finalizedWithdrawalTxns, prevTx.TxHash())
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
// Attach the finalized withdrawal tx to the deposits. After a client
|
|
|
|
|
// restart we can use this address as an indicator to republish the
|
|
|
|
|
// withdrawal tx and continue the withdrawal.
|
|
|
|
|
// Deposits with the same withdrawal tx are part of the same withdrawal.
|
|
|
|
|
for _, d := range deposits {
|
|
|
|
|
d.Lock()
|
|
|
|
|
d.FinalizedWithdrawalTx = finalizedTx
|
|
|
|
|
d.Unlock()
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
// Add the new withdrawal tx to the finalized withdrawals to republish
|
|
|
|
|
// it on block arrivals.
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
m.finalizedWithdrawalTxns[finalizedTx.TxHash()] = finalizedTx
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
// Transition the deposits to the withdrawing state. If the user fee
|
|
|
|
|
// bumped a withdrawal this results in a NOOP transition.
|
2024-05-06 14:19:07 +02:00
|
|
|
err = m.cfg.DepositManager.TransitionDeposits(
|
|
|
|
|
ctx, deposits, deposit.OnWithdrawInitiated, deposit.Withdrawing,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-03-19 13:36:49 +01:00
|
|
|
return "", "", fmt.Errorf("failed to transition deposits %w",
|
|
|
|
|
err)
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
// Update the deposits in the database.
|
|
|
|
|
for _, d := range deposits {
|
|
|
|
|
err = m.cfg.DepositManager.UpdateDeposit(ctx, d)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", "", fmt.Errorf("failed to update "+
|
|
|
|
|
"deposit %w", err)
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return finalizedTx.TxID(), withdrawalAddress.String(), nil
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// CreateFinalizedWithdrawalTx creates and signs a finalized withdrawal
|
|
|
|
|
// transaction that can be broadcast to the network. It returns the
|
|
|
|
|
// signed *wire.MsgTx representation and the unsigned psbt.
|
|
|
|
|
func (m *Manager) CreateFinalizedWithdrawalTx(ctx context.Context,
|
2024-11-29 14:47:51 +01:00
|
|
|
deposits []*deposit.Deposit, withdrawalAddress btcutil.Address,
|
2025-05-21 14:17:14 +02:00
|
|
|
feeRate chainfee.SatPerKWeight,
|
|
|
|
|
selectedWithdrawalAmount int64,
|
|
|
|
|
commitmentType lnrpc.CommitmentType) (*wire.MsgTx, []byte, error) {
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
// Create a musig2 session for each deposit.
|
2025-11-17 14:30:49 +01:00
|
|
|
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sessions, clientNonces, idx, err := staticutil.CreateMusig2SessionsPerDeposit(
|
|
|
|
|
ctx, m.cfg.Signer, deposits, addrParams, staticAddress,
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, err
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2024-12-03 13:44:04 +01:00
|
|
|
params, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
2024-12-14 12:02:49 +01:00
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, fmt.Errorf("couldn't get confirmation "+
|
|
|
|
|
"height for deposit, %w", err)
|
2024-12-14 12:02:49 +01:00
|
|
|
}
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
outpoints := toOutpoints(deposits)
|
2025-11-17 14:30:49 +01:00
|
|
|
prevOuts, err := staticutil.ToPrevOuts(deposits, params.PkScript)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
withdrawalTx, unsignedPsbt, err := m.createWithdrawalTx(
|
|
|
|
|
ctx, outpoints, deposits, prevOuts,
|
2024-12-03 13:44:04 +01:00
|
|
|
btcutil.Amount(selectedWithdrawalAmount), withdrawalAddress,
|
2025-05-21 14:17:14 +02:00
|
|
|
feeRate, commitmentType,
|
2024-12-03 13:44:04 +01:00
|
|
|
)
|
2024-12-14 12:02:49 +01:00
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, err
|
2024-12-14 12:02:49 +01:00
|
|
|
}
|
|
|
|
|
|
2024-12-03 13:44:04 +01:00
|
|
|
// Request the server to sign the withdrawal transaction.
|
|
|
|
|
//
|
|
|
|
|
// The withdrawal and change amount are sent to the server with the
|
|
|
|
|
// expectation that the server just signs the transaction, without
|
|
|
|
|
// performing fee calculations and dust considerations. The client is
|
|
|
|
|
// responsible for that.
|
2025-11-17 14:30:49 +01:00
|
|
|
// nolint:lll
|
|
|
|
|
sigResp, err := m.cfg.StaticAddressServerClient.ServerPsbtWithdrawDeposits(
|
|
|
|
|
ctx, &staticaddressrpc.ServerPsbtWithdrawRequest{
|
|
|
|
|
WithdrawalPsbt: unsignedPsbt,
|
|
|
|
|
DepositToNonces: clientNonces,
|
2024-05-06 14:19:07 +02:00
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, err
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// Do some sanity checks.
|
|
|
|
|
txHash := withdrawalTx.TxHash()
|
|
|
|
|
if !bytes.Equal(txHash.CloneBytes(), sigResp.Txid) {
|
|
|
|
|
return nil, nil, errors.New("txid doesn't match")
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
if len(sigResp.SigningInfo) != len(deposits) {
|
|
|
|
|
return nil, nil, errors.New("invalid number of " +
|
|
|
|
|
"deposit signatures")
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// Verify 1:1 matching between deposits and SigningInfo entries.
|
|
|
|
|
// Each deposit must have exactly one corresponding entry in
|
|
|
|
|
// SigningInfo.
|
|
|
|
|
for _, d := range deposits {
|
|
|
|
|
depositKey := d.OutPoint.String()
|
|
|
|
|
if _, ok := sigResp.SigningInfo[depositKey]; !ok {
|
|
|
|
|
return nil, nil, fmt.Errorf("missing signature for "+
|
|
|
|
|
"deposit %s", depositKey)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Next we'll get our sweep tx signatures.
|
|
|
|
|
prevOutFetcher := txscript.NewMultiPrevOutFetcher(prevOuts)
|
|
|
|
|
finalizedTx, err := m.signMusig2Tx(
|
|
|
|
|
ctx, prevOutFetcher, m.cfg.Signer, withdrawalTx, sessions,
|
|
|
|
|
sigResp.SigningInfo, idx,
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, err
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
return finalizedTx, unsignedPsbt, nil
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) publishFinalizedWithdrawalTx(ctx context.Context,
|
2025-03-19 13:36:49 +01:00
|
|
|
tx *wire.MsgTx) (bool, error) {
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
if tx == nil {
|
2025-03-19 13:36:49 +01:00
|
|
|
return false, errors.New("can't publish, finalized " +
|
|
|
|
|
"withdrawal tx is nil")
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-06-22 22:29:07 -03:00
|
|
|
log.Debugf("Publishing deposit withdrawal with txid: %v ...",
|
|
|
|
|
tx.TxHash())
|
|
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
txLabel := fmt.Sprintf("deposit-withdrawal-%v", tx.TxHash())
|
|
|
|
|
|
|
|
|
|
// Publish the withdrawal sweep transaction.
|
|
|
|
|
err := m.cfg.WalletKit.PublishTransaction(ctx, tx, txLabel)
|
|
|
|
|
if err != nil {
|
2025-03-27 15:58:51 +01:00
|
|
|
if !strings.Contains(err.Error(), chain.ErrSameNonWitnessData.Error()) &&
|
|
|
|
|
!strings.Contains(err.Error(), "output already spent") &&
|
|
|
|
|
!strings.Contains(err.Error(), chain.ErrInsufficientFee.Error()) {
|
2025-03-19 13:36:49 +01:00
|
|
|
|
|
|
|
|
return false, err
|
|
|
|
|
} else {
|
2025-03-27 15:58:51 +01:00
|
|
|
if strings.Contains(err.Error(), "output already spent") {
|
|
|
|
|
log.Warnf("output already spent, tx %v, %v",
|
|
|
|
|
tx.TxHash(), err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
return false, nil
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
2025-03-19 13:36:49 +01:00
|
|
|
} else {
|
2025-06-22 22:29:07 -03:00
|
|
|
log.Debugf("Published deposit withdrawal with txid: %v",
|
2025-03-19 13:36:49 +01:00
|
|
|
tx.TxHash())
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
return true, nil
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
// handleWithdrawal starts a goroutine that listens for the spent of the first
|
|
|
|
|
// input of the withdrawal transaction.
|
2024-05-06 14:19:07 +02:00
|
|
|
func (m *Manager) handleWithdrawal(ctx context.Context,
|
|
|
|
|
deposits []*deposit.Deposit, txHash chainhash.Hash,
|
2025-03-19 13:36:49 +01:00
|
|
|
withdrawalPkscript []byte) error {
|
|
|
|
|
|
2025-06-02 15:49:03 +02:00
|
|
|
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
2024-05-06 14:19:07 +02:00
|
|
|
if err != nil {
|
2026-02-28 10:29:07 +01:00
|
|
|
log.Errorf("error retrieving address params: %v", err)
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-05-09 14:39:20 +02:00
|
|
|
return fmt.Errorf("withdrawal failed")
|
2025-03-19 13:36:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d := deposits[0]
|
|
|
|
|
spentChan, errChan, err := m.cfg.ChainNotifier.RegisterSpendNtfn(
|
2025-05-09 14:39:20 +02:00
|
|
|
ctx, &d.OutPoint, addrParams.PkScript,
|
|
|
|
|
int32(d.ConfirmationHeight),
|
2025-03-19 13:36:49 +01:00
|
|
|
)
|
2026-02-28 10:28:42 +01:00
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("unable to register spend ntfn: %w", err)
|
|
|
|
|
}
|
2025-03-19 13:36:49 +01:00
|
|
|
|
2024-05-06 14:19:07 +02:00
|
|
|
go func() {
|
|
|
|
|
select {
|
2025-06-02 15:49:03 +02:00
|
|
|
case spentTx := <-spentChan:
|
|
|
|
|
spendingHeight := uint32(spentTx.SpendingHeight)
|
2025-03-19 13:36:49 +01:00
|
|
|
// If the transaction received one confirmation, we
|
|
|
|
|
// ensure re-org safety by waiting for some more
|
|
|
|
|
// confirmations.
|
2026-02-28 11:06:29 +01:00
|
|
|
confChan, confErrChan, err :=
|
2025-03-19 13:36:49 +01:00
|
|
|
m.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
2025-06-02 15:49:03 +02:00
|
|
|
ctx, spentTx.SpenderTxHash,
|
|
|
|
|
withdrawalPkscript, MinConfs,
|
2025-03-19 13:36:49 +01:00
|
|
|
int32(m.initiationHeight.Load()),
|
|
|
|
|
)
|
2026-02-28 11:06:29 +01:00
|
|
|
if err != nil {
|
|
|
|
|
// TODO(#1087): Retry registration on
|
|
|
|
|
// next block instead of giving up.
|
|
|
|
|
log.Errorf("Error registering confirmation "+
|
|
|
|
|
"notification: %v", err)
|
|
|
|
|
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
select {
|
2025-06-02 15:49:03 +02:00
|
|
|
case tx := <-confChan:
|
2025-03-19 13:36:49 +01:00
|
|
|
err = m.cfg.DepositManager.TransitionDeposits(
|
|
|
|
|
ctx, deposits, deposit.OnWithdrawn,
|
|
|
|
|
deposit.Withdrawn,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error transitioning "+
|
|
|
|
|
"deposits: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-02 15:49:03 +02:00
|
|
|
// Remove the withdrawal tx from the active
|
|
|
|
|
// withdrawals to stop republishing it on block
|
|
|
|
|
// arrivals.
|
2025-03-19 13:36:49 +01:00
|
|
|
m.mu.Lock()
|
|
|
|
|
delete(m.finalizedWithdrawalTxns, txHash)
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
2025-06-02 15:49:03 +02:00
|
|
|
// Persist info about the finalized withdrawal.
|
|
|
|
|
err = m.cfg.Store.UpdateWithdrawal(
|
|
|
|
|
ctx, deposits, tx.Tx, spendingHeight,
|
|
|
|
|
addrParams.PkScript,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error persisting "+
|
|
|
|
|
"withdrawal: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 11:06:29 +01:00
|
|
|
case err := <-confErrChan:
|
|
|
|
|
// TODO(#1087): Handle reorgs by retrying
|
|
|
|
|
// confirmation registration on next block.
|
2025-03-19 13:36:49 +01:00
|
|
|
log.Errorf("Error waiting for confirmation: %v",
|
2024-05-06 14:19:07 +02:00
|
|
|
err)
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
case <-ctx.Done():
|
|
|
|
|
log.Errorf("Withdrawal tx confirmation wait " +
|
|
|
|
|
"canceled")
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
case err := <-errChan:
|
2025-11-15 22:54:56 -03:00
|
|
|
log.Errorf("Error waiting for spending: %v", err)
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
log.Errorf("Withdrawal tx confirmation wait canceled")
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toOutpoints(deposits []*deposit.Deposit) []wire.OutPoint {
|
|
|
|
|
outpoints := make([]wire.OutPoint, len(deposits))
|
|
|
|
|
for i, d := range deposits {
|
|
|
|
|
outpoints[i] = wire.OutPoint{
|
|
|
|
|
Hash: d.Hash,
|
|
|
|
|
Index: d.Index,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return outpoints
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// signMusig2Tx adds the server nonces to the musig2 sessions and signs the
|
|
|
|
|
// transaction.
|
|
|
|
|
func (m *Manager) signMusig2Tx(ctx context.Context,
|
2025-11-17 14:30:49 +01:00
|
|
|
prevOutFetcher *txscript.MultiPrevOutFetcher,
|
2024-05-06 14:19:07 +02:00
|
|
|
signer lndclient.SignerClient, tx *wire.MsgTx,
|
2025-11-17 14:30:49 +01:00
|
|
|
sessions map[string]*input.MuSig2SessionInfo,
|
|
|
|
|
sigInfo map[string]*staticaddressrpc.ServerPsbtWithdrawSigningInfo,
|
|
|
|
|
depositsToIdx map[string]int) (*wire.MsgTx, error) {
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// Create our digest.
|
|
|
|
|
var sigHash [32]byte
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-12-03 13:54:09 +01:00
|
|
|
if len(sigInfo) != len(depositsToIdx) {
|
|
|
|
|
return nil, fmt.Errorf("unexpected number of partial " +
|
|
|
|
|
"signatures from server")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for txIndex, input := range tx.TxIn {
|
|
|
|
|
outpoint := input.PreviousOutPoint.String()
|
|
|
|
|
if i, ok := depositsToIdx[outpoint]; ok {
|
|
|
|
|
if i != txIndex {
|
|
|
|
|
return nil, fmt.Errorf("deposit index maps " +
|
|
|
|
|
"wrong tx index")
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil, fmt.Errorf("tx outpoint not in deposit index map")
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// We'll now add the nonce to our session and sign the tx.
|
|
|
|
|
for deposit, sigAndNonce := range sigInfo {
|
|
|
|
|
session, ok := sessions[deposit]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, errors.New("session not found")
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
nonce := [musig2.PubNonceSize]byte{}
|
|
|
|
|
copy(nonce[:], sigAndNonce.Nonce)
|
2024-05-06 14:19:07 +02:00
|
|
|
haveAllNonces, err := signer.MuSig2RegisterNonces(
|
2025-11-17 14:30:49 +01:00
|
|
|
ctx, session.SessionID,
|
|
|
|
|
[][musig2.PubNonceSize]byte{nonce},
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, fmt.Errorf("error registering nonces: "+
|
|
|
|
|
"%w", err)
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !haveAllNonces {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, errors.New("expected all nonces to be " +
|
|
|
|
|
"registered")
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
|
|
|
|
|
sigHashes, txscript.SigHashDefault, tx,
|
|
|
|
|
depositsToIdx[deposit], prevOutFetcher,
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, fmt.Errorf("error calculating taproot "+
|
|
|
|
|
"sig hash: %w", err)
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
copy(sigHash[:], taprootSigHash)
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// Sign the tx.
|
|
|
|
|
_, err = signer.MuSig2Sign(
|
|
|
|
|
ctx, session.SessionID, sigHash, false,
|
|
|
|
|
)
|
2024-05-06 14:19:07 +02:00
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, fmt.Errorf("error signing tx: %w", err)
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// Combine the signature with the client signature.
|
|
|
|
|
haveAllSigs, sig, err := signer.MuSig2CombineSig(
|
|
|
|
|
ctx, session.SessionID,
|
|
|
|
|
[][]byte{sigAndNonce.Sig},
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("error combining signature: "+
|
|
|
|
|
"%w", err)
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
if !haveAllSigs {
|
|
|
|
|
return nil, errors.New("expected all signatures to " +
|
|
|
|
|
"be combined")
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
tx.TxIn[depositsToIdx[deposit]].Witness = wire.TxWitness{
|
|
|
|
|
sig,
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
return tx, nil
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2024-12-03 13:44:04 +01:00
|
|
|
func (m *Manager) createWithdrawalTx(ctx context.Context,
|
2025-11-17 14:30:49 +01:00
|
|
|
outpoints []wire.OutPoint, deposits []*deposit.Deposit,
|
|
|
|
|
prevOuts map[wire.OutPoint]*wire.TxOut,
|
2024-12-03 13:44:04 +01:00
|
|
|
selectedWithdrawalAmount btcutil.Amount, withdrawAddr btcutil.Address,
|
2025-05-21 14:17:14 +02:00
|
|
|
feeRate chainfee.SatPerKWeight,
|
|
|
|
|
commitmentType lnrpc.CommitmentType) (*wire.MsgTx, []byte, error) {
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
// First Create the tx.
|
|
|
|
|
msgTx := wire.NewMsgTx(2)
|
|
|
|
|
|
|
|
|
|
// Add the deposit inputs to the transaction in the order the server
|
|
|
|
|
// signed them.
|
|
|
|
|
for _, outpoint := range outpoints {
|
|
|
|
|
msgTx.AddTxIn(&wire.TxIn{
|
|
|
|
|
PreviousOutPoint: outpoint,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
withdrawalAmount, changeAmount, err := CalculateWithdrawalTxValues(
|
|
|
|
|
deposits, selectedWithdrawalAmount, feeRate,
|
2025-05-21 14:17:14 +02:00
|
|
|
withdrawAddr, commitmentType,
|
2024-12-03 13:44:04 +01:00
|
|
|
)
|
2024-05-06 14:19:07 +02:00
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, fmt.Errorf("error calculating funding tx "+
|
|
|
|
|
"values: %w", err)
|
2024-12-03 13:44:04 +01:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// For the user's convenience, we check that the change amount is lower
|
2024-12-03 13:44:04 +01:00
|
|
|
// than each input's value. If the change amount is higher than an
|
2025-11-17 14:30:49 +01:00
|
|
|
// input's value, we wouldn't have to include that input in the
|
2024-12-03 13:44:04 +01:00
|
|
|
// transaction, saving fees.
|
|
|
|
|
for outpoint, txOut := range prevOuts {
|
|
|
|
|
if changeAmount >= btcutil.Amount(txOut.Value) {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, fmt.Errorf("change amount %v "+
|
|
|
|
|
"is higher than an input value %v of input %v",
|
2024-12-03 13:44:04 +01:00
|
|
|
changeAmount, btcutil.Amount(txOut.Value),
|
|
|
|
|
outpoint)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
withdrawScript, err := txscript.PayToAddrScript(withdrawAddr)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, err
|
2024-12-03 13:44:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the withdrawal output.
|
|
|
|
|
msgTx.AddTxOut(&wire.TxOut{
|
|
|
|
|
Value: int64(withdrawalAmount),
|
|
|
|
|
PkScript: withdrawScript,
|
|
|
|
|
})
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
if changeAmount > 0 {
|
2024-12-03 13:44:04 +01:00
|
|
|
// Send change back to the same static address.
|
|
|
|
|
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
|
|
|
|
|
if err != nil {
|
2025-05-13 11:48:52 -03:00
|
|
|
log.Errorf("error retrieving taproot address %v", err)
|
2024-12-03 13:44:04 +01:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, fmt.Errorf("withdrawal failed")
|
2024-12-03 13:44:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changeAddress, err := btcutil.NewAddressTaproot(
|
|
|
|
|
schnorr.SerializePubKey(staticAddress.TaprootKey),
|
|
|
|
|
m.cfg.ChainParams,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, err
|
2024-12-03 13:44:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changeScript, err := txscript.PayToAddrScript(changeAddress)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return nil, nil, err
|
2024-12-03 13:44:04 +01:00
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2024-12-03 13:44:04 +01:00
|
|
|
msgTx.AddTxOut(&wire.TxOut{
|
|
|
|
|
Value: int64(changeAmount),
|
|
|
|
|
PkScript: changeScript,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// Create psbt for the withdrawal.
|
|
|
|
|
psbtx, err := psbt.NewFromUnsignedTx(msgTx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
pInputs := make([]psbt.PInput, len(outpoints))
|
|
|
|
|
for i, op := range outpoints {
|
|
|
|
|
prevOut := prevOuts[op]
|
|
|
|
|
pInputs[i] = psbt.PInput{
|
|
|
|
|
WitnessUtxo: &wire.TxOut{
|
|
|
|
|
Value: prevOut.Value,
|
|
|
|
|
PkScript: prevOut.PkScript,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
psbtx.Inputs = pInputs
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// Serialize the psbt to send it to the client.
|
|
|
|
|
var psbtBuf bytes.Buffer
|
|
|
|
|
err = psbtx.Serialize(&psbtBuf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, nil, err
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
return msgTx, psbtBuf.Bytes(), nil
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-05-21 14:17:14 +02:00
|
|
|
// CalculateWithdrawalTxValues calculates the values of the withdrawal
|
|
|
|
|
// transaction. It returns the withdrawal amount, the change amount, and an
|
|
|
|
|
// error if any.
|
2025-11-17 14:30:49 +01:00
|
|
|
func CalculateWithdrawalTxValues(deposits []*deposit.Deposit,
|
2025-05-21 14:17:14 +02:00
|
|
|
selectedAmount btcutil.Amount, feeRate chainfee.SatPerKWeight,
|
2025-11-17 14:30:49 +01:00
|
|
|
withdrawalAddress btcutil.Address,
|
|
|
|
|
commitmentType lnrpc.CommitmentType) (btcutil.Amount, btcutil.Amount,
|
|
|
|
|
error) {
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
if withdrawalAddress == nil &&
|
|
|
|
|
commitmentType == lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
|
|
|
|
|
|
|
|
|
|
return 0, 0, fmt.Errorf("either address or commitment type " +
|
|
|
|
|
"must be specified")
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
var (
|
|
|
|
|
err error
|
|
|
|
|
withdrawalFundingAmt btcutil.Amount
|
|
|
|
|
changeAmount btcutil.Amount
|
|
|
|
|
dustLimit = lnwallet.DustLimitForSize(input.P2TRSize)
|
|
|
|
|
isChannelOpen = commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
totalDepositAmount := btcutil.Amount(0)
|
|
|
|
|
for _, d := range deposits {
|
|
|
|
|
totalDepositAmount += d.Value
|
2024-12-03 13:44:04 +01:00
|
|
|
}
|
|
|
|
|
|
2025-05-21 14:17:14 +02:00
|
|
|
// Estimate the withdrawal transaction fee without change.
|
2025-11-17 14:30:49 +01:00
|
|
|
hasChange := false
|
|
|
|
|
weight, err := WithdrawalTxWeight(
|
|
|
|
|
len(deposits), withdrawalAddress, commitmentType, hasChange,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, 0, err
|
|
|
|
|
}
|
|
|
|
|
feeWithoutChange := feeRate.FeeForWeight(weight)
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-05-21 14:17:14 +02:00
|
|
|
// If the user selected an amount to withdraw, check if a change output
|
|
|
|
|
// is needed.
|
|
|
|
|
if selectedAmount > 0 {
|
2025-11-17 14:30:49 +01:00
|
|
|
// Estimate the transaction weight with change.
|
|
|
|
|
hasChange = true
|
|
|
|
|
weightWithChange, err := WithdrawalTxWeight(
|
|
|
|
|
len(deposits), withdrawalAddress, commitmentType,
|
|
|
|
|
hasChange,
|
2024-05-06 14:19:07 +02:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2025-11-17 14:30:49 +01:00
|
|
|
return 0, 0, err
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
2025-11-17 14:30:49 +01:00
|
|
|
feeWithChange := feeRate.FeeForWeight(weightWithChange)
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// The available change that can cover fees is the total
|
2025-05-21 14:17:14 +02:00
|
|
|
// selected deposit amount minus the selected amount.
|
|
|
|
|
change := totalDepositAmount - selectedAmount
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
switch {
|
|
|
|
|
case change-feeWithChange >= dustLimit:
|
|
|
|
|
// If the change can cover the fees without turning into
|
|
|
|
|
// dust, add a non-dust change output.
|
|
|
|
|
changeAmount = change - feeWithChange
|
2025-05-21 14:17:14 +02:00
|
|
|
withdrawalFundingAmt = selectedAmount
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
case change-feeWithoutChange >= 0:
|
|
|
|
|
// If the change is dust, we give it to the miners.
|
2025-05-21 14:17:14 +02:00
|
|
|
withdrawalFundingAmt = selectedAmount
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
default:
|
2025-05-21 14:17:14 +02:00
|
|
|
// If the fees eat into our selected amount, we fail the
|
|
|
|
|
// withdrawal.
|
2025-11-17 14:30:49 +01:00
|
|
|
return 0, 0, fmt.Errorf("the change doesn't " +
|
|
|
|
|
"cover for fees. Consider lowering the fee " +
|
2025-05-21 14:17:14 +02:00
|
|
|
"rate or decrease the selected amount")
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
2025-11-17 14:30:49 +01:00
|
|
|
} else {
|
2025-05-21 14:17:14 +02:00
|
|
|
// If the user wants to withdraw the total value of deposits, we
|
|
|
|
|
// don't need a change output.
|
2025-11-17 14:30:49 +01:00
|
|
|
withdrawalFundingAmt = totalDepositAmount - feeWithoutChange
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
if withdrawalFundingAmt < dustLimit {
|
|
|
|
|
return 0, 0, fmt.Errorf("withdrawal amount is below dust limit")
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
if changeAmount < 0 {
|
|
|
|
|
return 0, 0, fmt.Errorf("change amount is negative")
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-05-21 14:17:14 +02:00
|
|
|
// In case of a channel open, ensure that the channel funding amount is
|
|
|
|
|
// at least in the amount of lnd's minimum channel size.
|
2025-11-17 14:30:49 +01:00
|
|
|
if isChannelOpen && withdrawalFundingAmt < funding.MinChanFundingSize {
|
|
|
|
|
return 0, 0, fmt.Errorf("channel funding amount %v is lower "+
|
|
|
|
|
"than the minimum channel funding size %v",
|
|
|
|
|
withdrawalFundingAmt, funding.MinChanFundingSize)
|
|
|
|
|
}
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// For the user's convenience, we check that the change amount is lower
|
|
|
|
|
// than each input's value. If the change amount is higher than an
|
|
|
|
|
// input's value, we wouldn't have to include that input in the
|
|
|
|
|
// transaction, saving fees.
|
|
|
|
|
for _, d := range deposits {
|
|
|
|
|
if changeAmount >= d.Value {
|
|
|
|
|
return 0, 0, fmt.Errorf("change amount %v is "+
|
|
|
|
|
"higher than an input value %v of input %v",
|
|
|
|
|
changeAmount, d.Value, d.OutPoint.String())
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
return withdrawalFundingAmt, changeAmount, nil
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// WithdrawalTxWeight returns the weight for the withdrawal transaction.
|
|
|
|
|
func WithdrawalTxWeight(numInputs int, sweepAddress btcutil.Address,
|
|
|
|
|
commitmentType lnrpc.CommitmentType,
|
|
|
|
|
hasChange bool) (lntypes.WeightUnit, error) {
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
var weightEstimator input.TxWeightEstimator
|
2026-03-05 12:18:05 +01:00
|
|
|
for range numInputs {
|
2025-11-17 14:30:49 +01:00
|
|
|
weightEstimator.AddTaprootKeySpendInput(
|
|
|
|
|
txscript.SigHashDefault,
|
|
|
|
|
)
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
if commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE {
|
|
|
|
|
switch commitmentType {
|
|
|
|
|
case lnrpc.CommitmentType_SIMPLE_TAPROOT:
|
|
|
|
|
weightEstimator.AddP2TROutput()
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
default:
|
|
|
|
|
weightEstimator.AddP2WSHOutput()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Get the weight of the sweep output.
|
|
|
|
|
switch sweepAddress.(type) {
|
|
|
|
|
case *btcutil.AddressWitnessPubKeyHash:
|
|
|
|
|
weightEstimator.AddP2WKHOutput()
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
case *btcutil.AddressWitnessScriptHash:
|
|
|
|
|
weightEstimator.AddP2WSHOutput()
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
case *btcutil.AddressTaproot:
|
|
|
|
|
weightEstimator.AddP2TROutput()
|
2024-05-06 14:19:07 +02:00
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
default:
|
|
|
|
|
return 0, fmt.Errorf("invalid sweep address type %T",
|
|
|
|
|
sweepAddress)
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-17 14:30:49 +01:00
|
|
|
// If there's a change output add the weight of the static address.
|
|
|
|
|
if hasChange {
|
|
|
|
|
weightEstimator.AddP2TROutput()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return weightEstimator.Weight(), nil
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Manager) republishWithdrawals(ctx context.Context) error {
|
2025-03-19 13:36:49 +01:00
|
|
|
m.mu.Lock()
|
|
|
|
|
txns := make([]*wire.MsgTx, 0, len(m.finalizedWithdrawalTxns))
|
|
|
|
|
for _, tx := range m.finalizedWithdrawalTxns {
|
|
|
|
|
txns = append(txns, tx)
|
|
|
|
|
}
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
for _, finalizedTx := range txns {
|
2024-05-06 14:19:07 +02:00
|
|
|
if finalizedTx == nil {
|
|
|
|
|
log.Warnf("Finalized withdrawal tx is nil")
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-19 13:36:49 +01:00
|
|
|
_, err := m.publishFinalizedWithdrawalTx(ctx, finalizedTx)
|
2024-05-06 14:19:07 +02:00
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error republishing withdrawal: %v", err)
|
|
|
|
|
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeliverWithdrawalRequest forwards a withdrawal request to the manager main
|
|
|
|
|
// loop.
|
|
|
|
|
func (m *Manager) DeliverWithdrawalRequest(ctx context.Context,
|
2024-12-03 13:44:04 +01:00
|
|
|
outpoints []wire.OutPoint, destAddr string, satPerVbyte int64,
|
|
|
|
|
amount int64) (string, string, error) {
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
request := newWithdrawalRequest{
|
2024-11-29 14:47:51 +01:00
|
|
|
outpoints: outpoints,
|
|
|
|
|
destAddr: destAddr,
|
|
|
|
|
satPerVbyte: satPerVbyte,
|
2024-12-03 13:44:04 +01:00
|
|
|
amount: amount,
|
2024-11-29 14:47:51 +01:00
|
|
|
respChan: make(chan *newWithdrawalResponse),
|
2024-05-06 14:19:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send the new loop-in request to the manager run loop.
|
|
|
|
|
select {
|
|
|
|
|
case m.newWithdrawalRequestChan <- request:
|
|
|
|
|
|
|
|
|
|
case <-m.exitChan:
|
|
|
|
|
return "", "", fmt.Errorf("withdrawal manager has been " +
|
|
|
|
|
"canceled")
|
|
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return "", "", fmt.Errorf("context canceled while withdrawing")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Wait for the response from the manager run loop.
|
|
|
|
|
select {
|
|
|
|
|
case resp := <-request.respChan:
|
2025-04-23 21:33:08 -03:00
|
|
|
return resp.txHash, resp.withdrawalAddress, resp.err
|
2024-05-06 14:19:07 +02:00
|
|
|
|
|
|
|
|
case <-m.exitChan:
|
|
|
|
|
return "", "", fmt.Errorf("withdrawal manager has been " +
|
|
|
|
|
"canceled")
|
|
|
|
|
|
|
|
|
|
case <-ctx.Done():
|
|
|
|
|
return "", "", fmt.Errorf("context canceled while waiting " +
|
|
|
|
|
"for withdrawal response")
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-02 15:49:03 +02:00
|
|
|
|
|
|
|
|
// GetAllWithdrawals returns all finalized withdrawals from the store.
|
|
|
|
|
func (m *Manager) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) {
|
|
|
|
|
return m.cfg.Store.GetAllWithdrawals(ctx)
|
|
|
|
|
}
|