Skip to content

staticaddr: persist withdrawal info #938

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions cmd/loop/staticaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var staticAddressCommands = cli.Command{
newStaticAddressCommand,
listUnspentCommand,
listDepositsCommand,
listWithdrawalsCommand,
listStaticAddressSwapsCommand,
withdrawalCommand,
summaryCommand,
Expand Down Expand Up @@ -312,6 +313,38 @@ func listDeposits(ctx *cli.Context) error {
return nil
}

var listWithdrawalsCommand = cli.Command{
Name: "listwithdrawals",
Usage: "Display a summary of past withdrawals.",
Description: `
`,
Action: listWithdrawals,
}

func listWithdrawals(ctx *cli.Context) error {
ctxb := context.Background()
if ctx.NArg() > 0 {
return cli.ShowCommandHelp(ctx, "withdrawals")
}

client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()

resp, err := client.ListStaticAddressWithdrawals(
ctxb, &looprpc.ListStaticAddressWithdrawalRequest{},
)
if err != nil {
return err
}

printRespJSON(resp)

return nil
}

var listStaticAddressSwapsCommand = cli.Command{
Name: "listswaps",
Usage: "Shows a list of finalized static address swaps.",
Expand Down
2 changes: 2 additions & 0 deletions loopd/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
depositManager = deposit.NewManager(depoCfg)

// Static address deposit withdrawal manager setup.
withdrawalStore := withdraw.NewSqlStore(baseDb)
withdrawalCfg := &withdraw.ManagerConfig{
StaticAddressServerClient: staticAddressClient,
AddressManager: staticAddressManager,
Expand All @@ -612,6 +613,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
ChainParams: d.lnd.ChainParams,
ChainNotifier: d.lnd.ChainNotifier,
Signer: d.lnd.Signer,
Store: withdrawalStore,
}
withdrawalManager = withdraw.NewManager(withdrawalCfg, blockHeight)

Expand Down
35 changes: 35 additions & 0 deletions loopd/swapclient_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1666,6 +1666,41 @@ func (s *swapClientServer) ListStaticAddressDeposits(ctx context.Context,
}, nil
}

// ListStaticAddressWithdrawals returns a list of all finalized withdrawal
// transactions.
func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
_ *looprpc.ListStaticAddressWithdrawalRequest) (
*looprpc.ListStaticAddressWithdrawalResponse, error) {

withdrawals, err := s.withdrawalManager.GetAllWithdrawals(ctx)
if err != nil {
return nil, err
}

if len(withdrawals) == 0 {
return &looprpc.ListStaticAddressWithdrawalResponse{}, nil
}

clientWithdrawals := make(
[]*looprpc.StaticAddressWithdrawal, 0, len(withdrawals),
)
for _, w := range withdrawals {
withdrawal := &looprpc.StaticAddressWithdrawal{
TxId: w.TxID.String(),
Outpoints: w.DepositOutpoints,
TotalDepositAmountSatoshis: int64(w.TotalDepositAmount),
WithdrawnAmountSatoshis: int64(w.WithdrawnAmount),
ChangeAmountSatoshis: int64(w.ChangeAmount),
ConfirmationHeight: w.ConfirmationHeight,
}
clientWithdrawals = append(clientWithdrawals, withdrawal)
}

return &looprpc.ListStaticAddressWithdrawalResponse{
Withdrawals: clientWithdrawals,
}, nil
}

// ListStaticAddressSwaps returns a list of all swaps that are currently pending
// or previously succeeded.
func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context,
Expand Down
31 changes: 27 additions & 4 deletions staticaddr/withdraw/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ type ManagerConfig struct {

// Signer is the signer client that is used to sign transactions.
Signer lndclient.SignerClient

// Store is the store that is used to persist the finalized withdrawal
// transactions.
Store *SqlStore
}

// newWithdrawalRequest is used to send withdrawal request to the manager main
Expand Down Expand Up @@ -609,7 +613,8 @@ func (m *Manager) handleWithdrawal(ctx context.Context,

go func() {
select {
case <-spentChan:
case spentTx := <-spentChan:
spendingHeight := uint32(spentTx.SpendingHeight)
// If the transaction received one confirmation, we
// ensure re-org safety by waiting for some more
// confirmations.
Expand All @@ -621,7 +626,7 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
int32(m.initiationHeight.Load()),
)
select {
case <-confChan:
case tx := <-confChan:
err = m.cfg.DepositManager.TransitionDeposits(
ctx, deposits, deposit.OnWithdrawn,
deposit.Withdrawn,
Expand All @@ -631,12 +636,23 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
"deposits: %v", err)
}

// Remove the withdrawal tx from the active withdrawals
// to stop republishing it on block arrivals.
// Remove the withdrawal tx from the active
// withdrawals to stop republishing it on block
// arrivals.
m.mu.Lock()
delete(m.finalizedWithdrawalTxns, txHash)
m.mu.Unlock()

// Persist info about the finalized withdrawal.
err = m.cfg.Store.CreateWithdrawal(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nicer to add the withdrawal at the time it is requested then later on set it to confirmed.

ctx, tx.Tx, spendingHeight, deposits,
addrParams.PkScript,
)
if err != nil {
log.Errorf("Error persisting "+
"withdrawal: %v", err)
}

case err := <-errChan:
log.Errorf("Error waiting for confirmation: %v",
err)
Expand Down Expand Up @@ -1116,3 +1132,10 @@ func (m *Manager) DeliverWithdrawalRequest(ctx context.Context,
"for withdrawal response")
}
}

// GetAllWithdrawals returns all finalized withdrawals from the store.
func (m *Manager) GetAllWithdrawals(ctx context.Context) ([]*Withdrawal,
error) {

return m.cfg.Store.AllWithdrawals(ctx)
}
119 changes: 119 additions & 0 deletions staticaddr/withdraw/sql_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package withdraw

import (
"bytes"
"context"
"strings"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/staticaddr/deposit"
"github.com/lightningnetwork/lnd/clock"
)

// SqlStore is the backing store for static address withdrawals.
type SqlStore struct {
baseDB *loopdb.BaseDB
Copy link
Member

@sputn1ck sputn1ck May 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this exposes the full queries to this package, which IMO is not neccessary. as we usually define a Querier interface.


clock clock.Clock
}

// NewSqlStore constructs a new SQLStore from a BaseDB. The BaseDB is agnostic
// to the underlying driver which can be postgres or sqlite.
func NewSqlStore(db *loopdb.BaseDB) *SqlStore {
return &SqlStore{
baseDB: db,

clock: clock.NewDefaultClock(),
}
}

// CreateWithdrawal creates a static address withdrawal record in the database.
func (s *SqlStore) CreateWithdrawal(ctx context.Context, tx *wire.MsgTx,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose adding a light unit test for future regressions.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

confirmationHeight uint32, deposits []*deposit.Deposit,
changePkScript []byte) error {

strOutpoints := make([]string, len(deposits))
totalAmount := int64(0)
for i, deposit := range deposits {
strOutpoints[i] = deposit.OutPoint.String()
totalAmount += int64(deposit.Value)
}

// Populate the optional change amount.
withdrawnAmount, changeAmount := int64(0), int64(0)
if len(tx.TxOut) == 1 {
withdrawnAmount = tx.TxOut[0].Value
} else if len(tx.TxOut) == 2 {
withdrawnAmount, changeAmount = tx.TxOut[0].Value, tx.TxOut[1].Value
if bytes.Equal(changePkScript, tx.TxOut[0].PkScript) {
changeAmount = tx.TxOut[0].Value
withdrawnAmount = tx.TxOut[1].Value
}
}

createArgs := sqlc.CreateWithdrawalParams{
WithdrawalTxID: tx.TxHash().String(),
DepositOutpoints: strings.Join(strOutpoints, ","),
TotalDepositAmount: totalAmount,
WithdrawnAmount: withdrawnAmount,
ChangeAmount: changeAmount,
ConfirmationHeight: int64(confirmationHeight),
}

return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{},
func(q *sqlc.Queries) error {
return q.CreateWithdrawal(ctx, createArgs)
})
}

// AllWithdrawals retrieves all known withdrawals.
func (s *SqlStore) AllWithdrawals(ctx context.Context) ([]*Withdrawal, error) {
var allWithdrawals []*Withdrawal

err := s.baseDB.ExecTx(ctx, loopdb.NewSqlReadOpts(),
func(q *sqlc.Queries) error {
var err error

withdrawals, err := q.AllWithdrawals(ctx)
if err != nil {
return err
}

for _, withdrawal := range withdrawals {
w, err := s.toWithdrawal(withdrawal)
if err != nil {
return err
}

allWithdrawals = append(allWithdrawals, w)
}

return nil
})
if err != nil {
return nil, err
}

return allWithdrawals, nil
}

// toDeposit converts an sql deposit to a deposit.
func (s *SqlStore) toWithdrawal(row sqlc.Withdrawal) (*Withdrawal, error) {
txHash, err := chainhash.NewHashFromStr(row.WithdrawalTxID)
if err != nil {
return nil, err
}

return &Withdrawal{
TxID: *txHash,
DepositOutpoints: strings.Split(row.DepositOutpoints, ","),
TotalDepositAmount: btcutil.Amount(row.TotalDepositAmount),
WithdrawnAmount: btcutil.Amount(row.WithdrawnAmount),
ChangeAmount: btcutil.Amount(row.ChangeAmount),
ConfirmationHeight: uint32(row.ConfirmationHeight),
}, nil
}
32 changes: 32 additions & 0 deletions staticaddr/withdraw/withdrawal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package withdraw

import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)

// Withdrawal represents a finalized static address withdrawal record in the
// database.
type Withdrawal struct {
// TxID is the transaction ID of the withdrawal.
TxID chainhash.Hash

// DepositOutpoints is a list of outpoints that were used to fund the
// withdrawal.
DepositOutpoints []string

// TotalDepositAmount is the total amount of all deposits used to fund
// the withdrawal.
TotalDepositAmount btcutil.Amount

// WithdrawnAmount is the amount withdrawn. It represents the total
// value of selected deposits minus fees and change.
WithdrawnAmount btcutil.Amount

// ChangeAmount is the optional change returned to the static address.
ChangeAmount btcutil.Amount

// ConfirmationHeight is the block height at which the withdrawal was
// confirmed.
ConfirmationHeight uint32
}