mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
staticaddr: store historic withdrawal info
This commit is contained in:
parent
cd9bcdf6f7
commit
5718f11162
5 changed files with 437 additions and 9 deletions
|
|
@ -11,6 +11,18 @@ import (
|
|||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
)
|
||||
|
||||
// Store is the database interface that is used to store and retrieve
|
||||
// static address withdrawals.
|
||||
type Store interface {
|
||||
// CreateWithdrawal inserts a withdrawal into the store.
|
||||
CreateWithdrawal(ctx context.Context, tx *wire.MsgTx,
|
||||
confirmationHeight uint32, deposits []*deposit.Deposit,
|
||||
changePkScript []byte) error
|
||||
|
||||
// GetAllWithdrawals retrieves all withdrawals.
|
||||
GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error)
|
||||
}
|
||||
|
||||
// AddressManager handles fetching of address parameters.
|
||||
type AddressManager interface {
|
||||
// GetStaticAddressParameters returns the static address parameters.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -401,6 +405,13 @@ func (m *Manager) WithdrawDeposits(ctx context.Context,
|
|||
// republished in case of a fee bump, it suffices if only one spent
|
||||
// notifier is run.
|
||||
if allDeposited {
|
||||
// Persist info about the finalized withdrawal.
|
||||
err = m.cfg.Store.CreateWithdrawal(ctx, deposits)
|
||||
if err != nil {
|
||||
log.Errorf("Error persisting "+
|
||||
"withdrawal: %v", err)
|
||||
}
|
||||
|
||||
err = m.handleWithdrawal(
|
||||
ctx, deposits, finalizedTx.TxHash(), withdrawalPkScript,
|
||||
)
|
||||
|
|
@ -592,9 +603,7 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
|
|||
deposits []*deposit.Deposit, txHash chainhash.Hash,
|
||||
withdrawalPkscript []byte) error {
|
||||
|
||||
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(
|
||||
ctx,
|
||||
)
|
||||
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
|
||||
if err != nil {
|
||||
log.Errorf("error retrieving address params %w", err)
|
||||
|
||||
|
|
@ -609,19 +618,20 @@ 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.
|
||||
var confChan chan *chainntnfs.TxConfirmation
|
||||
confChan, errChan, err =
|
||||
m.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
||||
ctx, &txHash, withdrawalPkscript,
|
||||
MinConfs,
|
||||
ctx, spentTx.SpenderTxHash,
|
||||
withdrawalPkscript, MinConfs,
|
||||
int32(m.initiationHeight.Load()),
|
||||
)
|
||||
select {
|
||||
case <-confChan:
|
||||
case tx := <-confChan:
|
||||
err = m.cfg.DepositManager.TransitionDeposits(
|
||||
ctx, deposits, deposit.OnWithdrawn,
|
||||
deposit.Withdrawn,
|
||||
|
|
@ -631,12 +641,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.UpdateWithdrawal(
|
||||
ctx, deposits, tx.Tx, spendingHeight,
|
||||
addrParams.PkScript,
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("Error persisting "+
|
||||
"withdrawal: %v", err)
|
||||
}
|
||||
|
||||
case err := <-errChan:
|
||||
log.Errorf("Error waiting for confirmation: %v",
|
||||
err)
|
||||
|
|
@ -1116,3 +1137,8 @@ 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.GetAllWithdrawals(ctx)
|
||||
}
|
||||
|
|
|
|||
221
staticaddr/withdraw/sql_store.go
Normal file
221
staticaddr/withdraw/sql_store.go
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
package withdraw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
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 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 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 updates a withdrawal record with the 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 static address withdrawals from the
|
||||
// database. It returns a slice of Withdrawal structs, each containing a list
|
||||
// of associated deposits.
|
||||
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)
|
||||
}
|
||||
|
||||
txID, err := chainhash.NewHashFromStr(w.WithdrawalTxID.String)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
101
staticaddr/withdraw/sql_store_test.go
Normal file
101
staticaddr/withdraw/sql_store_test.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
package withdraw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSqlStore tests the basic functionality of the SQLStore.
|
||||
func TestSqlStore(t *testing.T) {
|
||||
ctxb := context.Background()
|
||||
testDb := loopdb.NewTestDB(t)
|
||||
defer testDb.Close()
|
||||
|
||||
depositStore := deposit.NewSqlStore(testDb.BaseDB)
|
||||
store := NewSqlStore(loopdb.NewTypedStore[Querier](testDb), depositStore)
|
||||
|
||||
newID := func() deposit.ID {
|
||||
did, err := deposit.GetRandomDepositID()
|
||||
require.NoError(t, err)
|
||||
|
||||
return did
|
||||
}
|
||||
|
||||
d1, d2 := &deposit.Deposit{
|
||||
ID: newID(),
|
||||
Value: btcutil.Amount(100_000),
|
||||
TimeOutSweepPkScript: []byte{
|
||||
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
|
||||
},
|
||||
},
|
||||
&deposit.Deposit{
|
||||
ID: newID(),
|
||||
Value: btcutil.Amount(200_000),
|
||||
TimeOutSweepPkScript: []byte{
|
||||
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d,
|
||||
},
|
||||
}
|
||||
|
||||
withdrawalTx := &wire.MsgTx{
|
||||
Version: 2,
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: int64(d1.Value + d2.Value - 100),
|
||||
PkScript: []byte{
|
||||
0x00,
|
||||
},
|
||||
},
|
||||
{
|
||||
Value: int64(100),
|
||||
PkScript: []byte{
|
||||
0x01,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := depositStore.CreateDeposit(ctxb, d1)
|
||||
require.NoError(t, err)
|
||||
err = depositStore.CreateDeposit(ctxb, d2)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.CreateWithdrawal(ctxb, []*deposit.Deposit{d1, d2})
|
||||
require.NoError(t, err)
|
||||
|
||||
withdrawals, err := store.GetAllWithdrawals(ctxb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, withdrawals, 1)
|
||||
require.NotEmpty(t, withdrawals[0].ID)
|
||||
require.EqualValues(
|
||||
t, d1.Value+d2.Value, withdrawals[0].TotalDepositAmount,
|
||||
)
|
||||
require.Len(t, withdrawals[0].Deposits, 2)
|
||||
require.EqualValues(
|
||||
t, d1.Value, withdrawals[0].Deposits[0].Value,
|
||||
)
|
||||
require.EqualValues(
|
||||
t, d2.Value, withdrawals[0].Deposits[1].Value,
|
||||
)
|
||||
require.NotEmpty(t, withdrawals[0].InitiationTime)
|
||||
|
||||
err = store.UpdateWithdrawal(
|
||||
ctxb, []*deposit.Deposit{d1, d2}, withdrawalTx, 6, []byte{0x01},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
withdrawals, err = store.GetAllWithdrawals(ctxb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, withdrawals, 1)
|
||||
require.NotEmpty(t, withdrawals[0].TxID)
|
||||
require.EqualValues(
|
||||
t, d1.Value+d2.Value-100, withdrawals[0].WithdrawnAmount,
|
||||
)
|
||||
require.EqualValues(t, 100, withdrawals[0].ChangeAmount)
|
||||
require.EqualValues(t, 6, withdrawals[0].ConfirmationHeight)
|
||||
}
|
||||
68
staticaddr/withdraw/withdrawal.go
Normal file
68
staticaddr/withdraw/withdrawal.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package withdraw
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
)
|
||||
|
||||
const (
|
||||
IdLength = 32
|
||||
)
|
||||
|
||||
// ID is a unique identifier for a deposit.
|
||||
type ID [IdLength]byte
|
||||
|
||||
// FromByteSlice creates a deposit id from a byte slice.
|
||||
func (r *ID) FromByteSlice(b []byte) error {
|
||||
if len(b) != IdLength {
|
||||
return fmt.Errorf("withdrawal id must be 32 bytes, got %d, %x",
|
||||
len(b), b)
|
||||
}
|
||||
|
||||
copy(r[:], b)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Withdrawal represents a finalized static address withdrawal record in the
|
||||
// database.
|
||||
type Withdrawal struct {
|
||||
// ID is the unique identifier of the deposit.
|
||||
ID ID
|
||||
|
||||
// TxID is the transaction ID of the withdrawal.
|
||||
TxID chainhash.Hash
|
||||
|
||||
// Deposits is a list of deposits used to fund the withdrawal.
|
||||
Deposits []*deposit.Deposit
|
||||
|
||||
// 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
|
||||
|
||||
// InitiationTime is the time at which the withdrawal was initiated.
|
||||
InitiationTime time.Time
|
||||
|
||||
// ConfirmationHeight is the block height at which the withdrawal was
|
||||
// confirmed.
|
||||
ConfirmationHeight int64
|
||||
}
|
||||
|
||||
// GetRandomWithdrawalID generates a random withdrawal ID.
|
||||
func GetRandomWithdrawalID() (ID, error) {
|
||||
var id ID
|
||||
_, err := rand.Read(id[:])
|
||||
return id, err
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue