loop/staticaddr/withdraw/sql_store.go
Boris Nagaev 8ac8e3801c
staticaddr: model pending withdrawals
Represent withdrawal txids as optional in the domain model and keep
pending withdrawals visible through GetAllWithdrawals.

Pending rows now surface nil and zero values in Go and empty and zero
defaults over RPC, while malformed non-NULL txids still fail loudly.

Also sync godoc's with how it actually behaves: returns pending withdraws
in addition to finalized ones.
2026-04-07 01:47:31 -05:00

233 lines
6.4 KiB
Go

package withdraw
import (
"bytes"
"context"
"database/sql"
"fmt"
"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/lightninglabs/loop/utils/chainhashutil"
"github.com/lightningnetwork/lnd/clock"
)
type Querier interface {
// CreateWithdrawal inserts a new withdrawal.
CreateWithdrawal(ctx context.Context,
arg sqlc.CreateWithdrawalParams) error
// UpdateWithdrawal updates a withdrawal with confirmation parameters.
UpdateWithdrawal(ctx context.Context,
arg sqlc.UpdateWithdrawalParams) error
// GetWithdrawalIDByDepositID retrieves the withdrawal ID associated
// with a given deposit ID.
GetWithdrawalIDByDepositID(ctx context.Context, depositID []byte) (
[]byte, error)
// CreateWithdrawalDeposit links withdrawal to deposits.
CreateWithdrawalDeposit(ctx context.Context,
arg sqlc.CreateWithdrawalDepositParams) error
// GetWithdrawalDeposits retrieves the deposit IDs associated with a
// withdrawal.
GetWithdrawalDeposits(ctx context.Context, withdrawalID []byte) (
[][]byte, error)
// GetAllWithdrawals retrieves all pending and finalized withdrawals from
// the database.
GetAllWithdrawals(ctx context.Context) ([]sqlc.Withdrawal, error)
}
// BaseDB is the interface that contains all the queries generated by sqlc for
// the static_address_swaps table and transaction functionality.
type BaseDB interface {
Querier
// ExecTx allows for executing a function in the context of a database
// transaction.
ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
txBody func(Querier) error) error
}
// SqlStore is the backing store for static address withdrawals.
type SqlStore struct {
baseDB BaseDB
depositStore deposit.Store
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 BaseDB, depositStore deposit.Store) *SqlStore {
return &SqlStore{
baseDB: db,
depositStore: depositStore,
clock: clock.NewDefaultClock(),
}
}
// CreateWithdrawal creates a pending static address withdrawal record in the
// database.
func (s *SqlStore) CreateWithdrawal(ctx context.Context,
deposits []*deposit.Deposit) error {
id, err := GetRandomWithdrawalID()
if err != nil {
return err
}
var totalAmount btcutil.Amount
for _, deposit := range deposits {
totalAmount += deposit.Value
}
createArgs := sqlc.CreateWithdrawalParams{
WithdrawalID: id[:],
TotalDepositAmount: int64(totalAmount),
InitiationTime: s.clock.Now().UTC(),
}
return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{},
func(q Querier) error {
err := q.CreateWithdrawal(ctx, createArgs)
if err != nil {
return err
}
for _, deposit := range deposits {
err = q.CreateWithdrawalDeposit(
ctx, sqlc.CreateWithdrawalDepositParams{
WithdrawalID: id[:],
DepositID: deposit.ID[:],
})
if err != nil {
return err
}
}
return nil
})
}
// UpdateWithdrawal finalizes a pending withdrawal record with the confirmed
// transaction information, including the withdrawn amount, change amount, and
// confirmation height. It is expected that the withdrawal has already been
// created with CreateWithdrawal, and that the deposits slice contains the
// deposits associated with the withdrawal.
func (s *SqlStore) UpdateWithdrawal(ctx context.Context,
deposits []*deposit.Deposit, tx *wire.MsgTx, confirmationHeight uint32,
changePkScript []byte) error {
// 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
}
}
updateArgs := sqlc.UpdateWithdrawalParams{
WithdrawalTxID: sql.NullString{
String: tx.TxHash().String(),
Valid: true,
},
WithdrawnAmount: sql.NullInt64{
Int64: withdrawnAmount,
Valid: withdrawnAmount > 0,
},
ChangeAmount: sql.NullInt64{
Int64: changeAmount,
Valid: changeAmount > 0,
},
ConfirmationHeight: sql.NullInt64{
Int64: int64(confirmationHeight),
Valid: confirmationHeight > 0,
},
}
return s.baseDB.ExecTx(ctx, &loopdb.SqliteTxOptions{},
func(q Querier) error {
withdrawalID, err := q.GetWithdrawalIDByDepositID(
ctx, deposits[0].ID[:],
)
if err != nil {
return err
}
updateArgs.WithdrawalID = withdrawalID
err = q.UpdateWithdrawal(ctx, updateArgs)
if err != nil {
return err
}
return nil
})
}
// GetAllWithdrawals retrieves all pending and finalized static address
// withdrawals from the database. Pending withdrawals return default zero
// values for fields that are only known after confirmation, and a nil TxID.
func (s *SqlStore) GetAllWithdrawals(ctx context.Context) ([]Withdrawal,
error) {
withdrawals, err := s.baseDB.GetAllWithdrawals(ctx)
if err != nil {
return nil, err
}
result := make([]Withdrawal, 0, len(withdrawals))
for _, w := range withdrawals {
depositIDs, err := s.baseDB.GetWithdrawalDeposits(ctx,
w.WithdrawalID)
if err != nil {
return nil, err
}
deposits := make([]*deposit.Deposit, 0, len(depositIDs))
for _, dID := range depositIDs {
deposit, err := s.depositStore.GetDeposit(
ctx, deposit.ID(dID),
)
if err != nil {
return nil, err
}
deposits = append(deposits, deposit)
}
var txID *chainhash.Hash
if w.WithdrawalTxID.Valid {
hash, err := chainhashutil.NewHashFromStrExact(
w.WithdrawalTxID.String,
)
if err != nil {
return nil, fmt.Errorf("invalid withdrawal txid %q: %w",
w.WithdrawalTxID.String, err)
}
txID = &hash
}
result = append(result, Withdrawal{
ID: ID(w.WithdrawalID),
TxID: txID,
Deposits: deposits,
TotalDepositAmount: btcutil.Amount(w.TotalDepositAmount),
WithdrawnAmount: btcutil.Amount(w.WithdrawnAmount.Int64),
ChangeAmount: btcutil.Amount(w.ChangeAmount.Int64),
InitiationTime: w.InitiationTime,
ConfirmationHeight: w.ConfirmationHeight.Int64,
})
}
return result, nil
}