Merge pull request #938 from hieblmi/withdraw-info

staticaddr: persist withdrawal info
This commit is contained in:
Slyghtning 2025-06-10 17:49:20 +02:00 committed by GitHub
commit 80828c272c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1855 additions and 639 deletions

View file

@ -30,6 +30,7 @@ var staticAddressCommands = cli.Command{
newStaticAddressCommand,
listUnspentCommand,
listDepositsCommand,
listWithdrawalsCommand,
listStaticAddressSwapsCommand,
withdrawalCommand,
summaryCommand,
@ -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.",

View file

@ -604,6 +604,10 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
depositManager = deposit.NewManager(depoCfg)
// Static address deposit withdrawal manager setup.
withdrawalStore := withdraw.NewSqlStore(
loopdb.NewTypedStore[withdraw.Querier](baseDb),
depositStore,
)
withdrawalCfg := &withdraw.ManagerConfig{
StaticAddressServerClient: staticAddressClient,
AddressManager: staticAddressManager,
@ -612,6 +616,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)

View file

@ -1673,6 +1673,53 @@ 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 {
deposits := make([]*looprpc.Deposit, 0, len(w.Deposits))
for _, d := range w.Deposits {
deposits = append(deposits, &looprpc.Deposit{
Id: d.ID[:],
Outpoint: d.OutPoint.String(),
Value: int64(d.Value),
ConfirmationHeight: d.ConfirmationHeight,
State: toClientDepositState(
d.GetState(),
),
})
}
withdrawal := &looprpc.StaticAddressWithdrawal{
TxId: w.TxID.String(),
Deposits: deposits,
TotalDepositAmountSatoshis: int64(w.TotalDepositAmount),
WithdrawnAmountSatoshis: int64(w.WithdrawnAmount),
ChangeAmountSatoshis: int64(w.ChangeAmount),
ConfirmationHeight: uint32(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,

View file

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS withdrawals;
DROP TABLE IF EXISTS withdrawal_deposits;

View file

@ -0,0 +1,42 @@
-- withdrawals stores finalized static address withdrawals.
CREATE TABLE IF NOT EXISTS withdrawals (
-- id is the auto-incrementing primary key for a withdrawal.
id INTEGER PRIMARY KEY,
-- withdrawal_id is the unique identifier for the withdrawal.
withdrawal_id BLOB NOT NULL UNIQUE,
-- withdrawal_tx_id is the transaction tx id of the withdrawal.
withdrawal_tx_id TEXT UNIQUE,
-- total_deposit_amount is the total amount of the deposits in satoshis.
total_deposit_amount BIGINT NOT NULL,
-- withdrawn_amount is the total amount of the withdrawal. It amounts
-- to the total amount of the deposits minus the fees and optional change.
withdrawn_amount BIGINT,
-- change_amount is the optional change that the user selected.
change_amount BIGINT,
-- initiation_time is the creation of the withdrawal.
initiation_time TIMESTAMP NOT NULL,
-- confirmation_height is the block height at which the withdrawal was first
-- confirmed.
confirmation_height BIGINT
);
CREATE TABLE IF NOT EXISTS withdrawal_deposits (
-- id is the auto-incrementing primary key.
id INTEGER PRIMARY KEY,
-- withdrawal_id references the withdrawals table.
withdrawal_id BLOB NOT NULL REFERENCES withdrawals(withdrawal_id),
-- deposit_id references the deposits table.
deposit_id BLOB NOT NULL REFERENCES deposits(deposit_id),
-- Ensure that each deposit is used only once per withdrawal.
UNIQUE(deposit_id, withdrawal_id)
);

View file

@ -208,3 +208,20 @@ type SweepsOld struct {
Amt int64
Completed bool
}
type Withdrawal struct {
ID int32
WithdrawalID []byte
WithdrawalTxID sql.NullString
TotalDepositAmount int64
WithdrawnAmount sql.NullInt64
ChangeAmount sql.NullInt64
InitiationTime time.Time
ConfirmationHeight sql.NullInt64
}
type WithdrawalDeposit struct {
ID int32
WithdrawalID []byte
DepositID []byte
}

View file

@ -16,8 +16,11 @@ type Querier interface {
CreateDeposit(ctx context.Context, arg CreateDepositParams) error
CreateReservation(ctx context.Context, arg CreateReservationParams) error
CreateStaticAddress(ctx context.Context, arg CreateStaticAddressParams) error
CreateWithdrawal(ctx context.Context, arg CreateWithdrawalParams) error
CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error
DropBatch(ctx context.Context, id int32) error
FetchLiquidityParams(ctx context.Context) ([]byte, error)
GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error)
GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error)
GetBatchSweptAmount(ctx context.Context, batchID int32) (int64, error)
GetDeposit(ctx context.Context, depositID []byte) (Deposit, error)
@ -42,6 +45,8 @@ type Querier interface {
GetSwapUpdates(ctx context.Context, swapHash []byte) ([]SwapUpdate, error)
GetSweepStatus(ctx context.Context, outpoint string) (bool, error)
GetUnconfirmedBatches(ctx context.Context) ([]SweepBatch, error)
GetWithdrawalDeposits(ctx context.Context, withdrawalID []byte) ([][]byte, error)
GetWithdrawalIDByDepositID(ctx context.Context, depositID []byte) ([]byte, error)
InsertBatch(ctx context.Context, arg InsertBatchParams) (int32, error)
InsertDepositUpdate(ctx context.Context, arg InsertDepositUpdateParams) error
InsertHtlcKeys(ctx context.Context, arg InsertHtlcKeysParams) error
@ -64,6 +69,7 @@ type Querier interface {
UpdateLoopOutAssetOffchainPayments(ctx context.Context, arg UpdateLoopOutAssetOffchainPaymentsParams) error
UpdateReservation(ctx context.Context, arg UpdateReservationParams) error
UpdateStaticAddressLoopIn(ctx context.Context, arg UpdateStaticAddressLoopInParams) error
UpdateWithdrawal(ctx context.Context, arg UpdateWithdrawalParams) error
UpsertLiquidityParams(ctx context.Context, params []byte) error
UpsertSweep(ctx context.Context, arg UpsertSweepParams) error
}

View file

@ -0,0 +1,47 @@
-- name: CreateWithdrawal :exec
INSERT INTO withdrawals (
withdrawal_id,
total_deposit_amount,
initiation_time
) VALUES (
$1, $2, $3
);
-- name: CreateWithdrawalDeposit :exec
INSERT INTO withdrawal_deposits (
withdrawal_id,
deposit_id
) VALUES (
$1, $2
);
-- name: GetWithdrawalIDByDepositID :one
SELECT withdrawal_id
FROM withdrawal_deposits
WHERE deposit_id = $1;
-- name: UpdateWithdrawal :exec
UPDATE withdrawals
SET
withdrawal_tx_id = $2,
withdrawn_amount = $3,
change_amount = $4,
confirmation_height = $5
WHERE
withdrawal_id = $1;
-- name: GetWithdrawalDeposits :many
SELECT
deposit_id
FROM
withdrawal_deposits
WHERE
withdrawal_id = $1;
-- name: GetAllWithdrawals :many
SELECT
*
FROM
withdrawals
ORDER BY
initiation_time DESC;

View file

@ -0,0 +1,168 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: static_address_withdrawals.sql
package sqlc
import (
"context"
"database/sql"
"time"
)
const createWithdrawal = `-- name: CreateWithdrawal :exec
INSERT INTO withdrawals (
withdrawal_id,
total_deposit_amount,
initiation_time
) VALUES (
$1, $2, $3
)
`
type CreateWithdrawalParams struct {
WithdrawalID []byte
TotalDepositAmount int64
InitiationTime time.Time
}
func (q *Queries) CreateWithdrawal(ctx context.Context, arg CreateWithdrawalParams) error {
_, err := q.db.ExecContext(ctx, createWithdrawal, arg.WithdrawalID, arg.TotalDepositAmount, arg.InitiationTime)
return err
}
const createWithdrawalDeposit = `-- name: CreateWithdrawalDeposit :exec
INSERT INTO withdrawal_deposits (
withdrawal_id,
deposit_id
) VALUES (
$1, $2
)
`
type CreateWithdrawalDepositParams struct {
WithdrawalID []byte
DepositID []byte
}
func (q *Queries) CreateWithdrawalDeposit(ctx context.Context, arg CreateWithdrawalDepositParams) error {
_, err := q.db.ExecContext(ctx, createWithdrawalDeposit, arg.WithdrawalID, arg.DepositID)
return err
}
const getAllWithdrawals = `-- name: GetAllWithdrawals :many
SELECT
id, withdrawal_id, withdrawal_tx_id, total_deposit_amount, withdrawn_amount, change_amount, initiation_time, confirmation_height
FROM
withdrawals
ORDER BY
initiation_time DESC
`
func (q *Queries) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) {
rows, err := q.db.QueryContext(ctx, getAllWithdrawals)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Withdrawal
for rows.Next() {
var i Withdrawal
if err := rows.Scan(
&i.ID,
&i.WithdrawalID,
&i.WithdrawalTxID,
&i.TotalDepositAmount,
&i.WithdrawnAmount,
&i.ChangeAmount,
&i.InitiationTime,
&i.ConfirmationHeight,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWithdrawalDeposits = `-- name: GetWithdrawalDeposits :many
SELECT
deposit_id
FROM
withdrawal_deposits
WHERE
withdrawal_id = $1
`
func (q *Queries) GetWithdrawalDeposits(ctx context.Context, withdrawalID []byte) ([][]byte, error) {
rows, err := q.db.QueryContext(ctx, getWithdrawalDeposits, withdrawalID)
if err != nil {
return nil, err
}
defer rows.Close()
var items [][]byte
for rows.Next() {
var deposit_id []byte
if err := rows.Scan(&deposit_id); err != nil {
return nil, err
}
items = append(items, deposit_id)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWithdrawalIDByDepositID = `-- name: GetWithdrawalIDByDepositID :one
SELECT withdrawal_id
FROM withdrawal_deposits
WHERE deposit_id = $1
`
func (q *Queries) GetWithdrawalIDByDepositID(ctx context.Context, depositID []byte) ([]byte, error) {
row := q.db.QueryRowContext(ctx, getWithdrawalIDByDepositID, depositID)
var withdrawal_id []byte
err := row.Scan(&withdrawal_id)
return withdrawal_id, err
}
const updateWithdrawal = `-- name: UpdateWithdrawal :exec
UPDATE withdrawals
SET
withdrawal_tx_id = $2,
withdrawn_amount = $3,
change_amount = $4,
confirmation_height = $5
WHERE
withdrawal_id = $1
`
type UpdateWithdrawalParams struct {
WithdrawalID []byte
WithdrawalTxID sql.NullString
WithdrawnAmount sql.NullInt64
ChangeAmount sql.NullInt64
ConfirmationHeight sql.NullInt64
}
func (q *Queries) UpdateWithdrawal(ctx context.Context, arg UpdateWithdrawalParams) error {
_, err := q.db.ExecContext(ctx, updateWithdrawal,
arg.WithdrawalID,
arg.WithdrawalTxID,
arg.WithdrawnAmount,
arg.ChangeAmount,
arg.ConfirmationHeight,
)
return err
}

File diff suppressed because it is too large Load diff

View file

@ -175,6 +175,12 @@ service SwapClient {
rpc ListStaticAddressDeposits (ListStaticAddressDepositsRequest)
returns (ListStaticAddressDepositsResponse);
/* loop:`listwithdrawals`
ListStaticAddressWithdrawals returns a list of static address withdrawals.
*/
rpc ListStaticAddressWithdrawals (ListStaticAddressWithdrawalRequest)
returns (ListStaticAddressWithdrawalResponse);
/* loop:`listswaps`
ListStaticAddressSwaps returns a list of filtered static address
swaps.
@ -1735,6 +1741,16 @@ message ListStaticAddressDepositsResponse {
repeated Deposit filtered_deposits = 1;
}
message ListStaticAddressWithdrawalRequest {
}
message ListStaticAddressWithdrawalResponse {
/*
A list of all static address withdrawals.
*/
repeated StaticAddressWithdrawal withdrawals = 1;
}
message ListStaticAddressSwapsRequest {
}
@ -1896,6 +1912,40 @@ message Deposit {
int64 blocks_until_expiry = 6;
}
message StaticAddressWithdrawal {
/*
The transaction id of the withdrawal transaction.
*/
string tx_id = 1;
/*
The selected deposits that is withdrawn from.
*/
repeated Deposit deposits = 2;
/*
The sum of the deposit values that was selected for withdrawal.
*/
int64 total_deposit_amount_satoshis = 3;
/*
The actual amount that was withdrawn from the selected deposits. This value
represents the sum of selected deposit values minus tx fees minus optional
change output.
*/
int64 withdrawn_amount_satoshis = 4;
/*
An optional change.
*/
int64 change_amount_satoshis = 5;
/*
The confirmation block height of the withdrawal transaction.
*/
uint32 confirmation_height = 6;
}
message StaticAddressLoopInSwap {
/*
The swap hash of the swap. It represents the unique identifier of the swap.

View file

@ -1392,6 +1392,18 @@
}
}
},
"looprpcListStaticAddressWithdrawalResponse": {
"type": "object",
"properties": {
"withdrawals": {
"type": "array",
"items": {
"$ref": "#/definitions/looprpcStaticAddressWithdrawal"
},
"description": "A list of all static address withdrawals."
}
}
},
"looprpcListSwapsFilter": {
"type": "object",
"properties": {
@ -1927,6 +1939,42 @@
}
}
},
"looprpcStaticAddressWithdrawal": {
"type": "object",
"properties": {
"tx_id": {
"type": "string",
"description": "The transaction id of the withdrawal transaction."
},
"deposits": {
"type": "array",
"items": {
"$ref": "#/definitions/looprpcDeposit"
},
"description": "The selected deposits that is withdrawn from."
},
"total_deposit_amount_satoshis": {
"type": "string",
"format": "int64",
"description": "The sum of the deposit values that was selected for withdrawal."
},
"withdrawn_amount_satoshis": {
"type": "string",
"format": "int64",
"description": "The actual amount that was withdrawn from the selected deposits. This value\nrepresents the sum of selected deposit values minus tx fees minus optional\nchange output."
},
"change_amount_satoshis": {
"type": "string",
"format": "int64",
"description": "An optional change."
},
"confirmation_height": {
"type": "integer",
"format": "int64",
"description": "The confirmation block height of the withdrawal transaction."
}
}
},
"looprpcSuggestSwapsResponse": {
"type": "object",
"properties": {

View file

@ -119,6 +119,9 @@ type SwapClientClient interface {
// ListStaticAddressDeposits returns a list of filtered static address
// deposits.
ListStaticAddressDeposits(ctx context.Context, in *ListStaticAddressDepositsRequest, opts ...grpc.CallOption) (*ListStaticAddressDepositsResponse, error)
// loop:`listwithdrawals`
// ListStaticAddressWithdrawals returns a list of static address withdrawals.
ListStaticAddressWithdrawals(ctx context.Context, in *ListStaticAddressWithdrawalRequest, opts ...grpc.CallOption) (*ListStaticAddressWithdrawalResponse, error)
// loop:`listswaps`
// ListStaticAddressSwaps returns a list of filtered static address
// swaps.
@ -397,6 +400,15 @@ func (c *swapClientClient) ListStaticAddressDeposits(ctx context.Context, in *Li
return out, nil
}
func (c *swapClientClient) ListStaticAddressWithdrawals(ctx context.Context, in *ListStaticAddressWithdrawalRequest, opts ...grpc.CallOption) (*ListStaticAddressWithdrawalResponse, error) {
out := new(ListStaticAddressWithdrawalResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/ListStaticAddressWithdrawals", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *swapClientClient) ListStaticAddressSwaps(ctx context.Context, in *ListStaticAddressSwapsRequest, opts ...grpc.CallOption) (*ListStaticAddressSwapsResponse, error) {
out := new(ListStaticAddressSwapsResponse)
err := c.cc.Invoke(ctx, "/looprpc.SwapClient/ListStaticAddressSwaps", in, out, opts...)
@ -529,6 +541,9 @@ type SwapClientServer interface {
// ListStaticAddressDeposits returns a list of filtered static address
// deposits.
ListStaticAddressDeposits(context.Context, *ListStaticAddressDepositsRequest) (*ListStaticAddressDepositsResponse, error)
// loop:`listwithdrawals`
// ListStaticAddressWithdrawals returns a list of static address withdrawals.
ListStaticAddressWithdrawals(context.Context, *ListStaticAddressWithdrawalRequest) (*ListStaticAddressWithdrawalResponse, error)
// loop:`listswaps`
// ListStaticAddressSwaps returns a list of filtered static address
// swaps.
@ -625,6 +640,9 @@ func (UnimplementedSwapClientServer) WithdrawDeposits(context.Context, *Withdraw
func (UnimplementedSwapClientServer) ListStaticAddressDeposits(context.Context, *ListStaticAddressDepositsRequest) (*ListStaticAddressDepositsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListStaticAddressDeposits not implemented")
}
func (UnimplementedSwapClientServer) ListStaticAddressWithdrawals(context.Context, *ListStaticAddressWithdrawalRequest) (*ListStaticAddressWithdrawalResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListStaticAddressWithdrawals not implemented")
}
func (UnimplementedSwapClientServer) ListStaticAddressSwaps(context.Context, *ListStaticAddressSwapsRequest) (*ListStaticAddressSwapsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListStaticAddressSwaps not implemented")
}
@ -1118,6 +1136,24 @@ func _SwapClient_ListStaticAddressDeposits_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler)
}
func _SwapClient_ListStaticAddressWithdrawals_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListStaticAddressWithdrawalRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwapClientServer).ListStaticAddressWithdrawals(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/looprpc.SwapClient/ListStaticAddressWithdrawals",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwapClientServer).ListStaticAddressWithdrawals(ctx, req.(*ListStaticAddressWithdrawalRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SwapClient_ListStaticAddressSwaps_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListStaticAddressSwapsRequest)
if err := dec(in); err != nil {
@ -1279,6 +1315,10 @@ var SwapClient_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListStaticAddressDeposits",
Handler: _SwapClient_ListStaticAddressDeposits_Handler,
},
{
MethodName: "ListStaticAddressWithdrawals",
Handler: _SwapClient_ListStaticAddressWithdrawals_Handler,
},
{
MethodName: "ListStaticAddressSwaps",
Handler: _SwapClient_ListStaticAddressSwaps_Handler,

View file

@ -101,6 +101,13 @@ var RequiredPermissions = map[string][]bakery.Op{
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/ListStaticAddressWithdrawals": {{
Entity: "swap",
Action: "read",
}, {
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/ListStaticAddressSwaps": {{
Entity: "swap",
Action: "read",

View file

@ -688,6 +688,31 @@ func RegisterSwapClientJSONCallbacks(registry map[string]func(ctx context.Contex
callback(string(respBytes), nil)
}
registry["looprpc.SwapClient.ListStaticAddressWithdrawals"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
req := &ListStaticAddressWithdrawalRequest{}
err := marshaler.Unmarshal([]byte(reqJSON), req)
if err != nil {
callback("", err)
return
}
client := NewSwapClientClient(conn)
resp, err := client.ListStaticAddressWithdrawals(ctx, req)
if err != nil {
callback("", err)
return
}
respBytes, err := marshaler.Marshal(resp)
if err != nil {
callback("", err)
return
}
callback(string(respBytes), nil)
}
registry["looprpc.SwapClient.ListStaticAddressSwaps"] = func(ctx context.Context,
conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {

View file

@ -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.

View file

@ -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,46 +603,35 @@ func (m *Manager) handleWithdrawal(ctx context.Context,
deposits []*deposit.Deposit, txHash chainhash.Hash,
withdrawalPkscript []byte) error {
staticAddress, err := m.cfg.AddressManager.GetStaticAddress(ctx)
addrParams, err := m.cfg.AddressManager.GetStaticAddressParameters(ctx)
if err != nil {
log.Errorf("error retrieving taproot address %w", err)
log.Errorf("error retrieving address params %w", err)
return fmt.Errorf("withdrawal failed")
}
address, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(staticAddress.TaprootKey),
m.cfg.ChainParams,
)
if err != nil {
return err
}
script, err := txscript.PayToAddrScript(address)
if err != nil {
return err
}
d := deposits[0]
spentChan, errChan, err := m.cfg.ChainNotifier.RegisterSpendNtfn(
ctx, &d.OutPoint, script, int32(d.ConfirmationHeight),
ctx, &d.OutPoint, addrParams.PkScript,
int32(d.ConfirmationHeight),
)
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,
@ -641,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)
@ -1126,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)
}

View 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
}

View 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)
}

View 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
}