mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
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.
This commit is contained in:
parent
e18ae71a26
commit
8ac8e3801c
10 changed files with 129 additions and 43 deletions
|
|
@ -1836,8 +1836,10 @@ func (s *swapClientServer) ListStaticAddressDeposits(ctx context.Context,
|
|||
}, nil
|
||||
}
|
||||
|
||||
// ListStaticAddressWithdrawals returns a list of all finalized withdrawal
|
||||
// transactions.
|
||||
// ListStaticAddressWithdrawals returns a list of all static address
|
||||
// withdrawals, including pending withdrawals. Pending withdrawals expose
|
||||
// default empty or zero values for fields that are only known after
|
||||
// confirmation.
|
||||
func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
|
||||
_ *looprpc.ListStaticAddressWithdrawalRequest) (
|
||||
*looprpc.ListStaticAddressWithdrawalResponse, error) {
|
||||
|
|
@ -1855,6 +1857,11 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
|
|||
[]*looprpc.StaticAddressWithdrawal, 0, len(withdrawals),
|
||||
)
|
||||
for _, w := range withdrawals {
|
||||
txID := ""
|
||||
if w.TxID != nil {
|
||||
txID = w.TxID.String()
|
||||
}
|
||||
|
||||
deposits := make([]*looprpc.Deposit, 0, len(w.Deposits))
|
||||
for _, d := range w.Deposits {
|
||||
deposits = append(deposits, &looprpc.Deposit{
|
||||
|
|
@ -1868,7 +1875,7 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
|
|||
})
|
||||
}
|
||||
withdrawal := &looprpc.StaticAddressWithdrawal{
|
||||
TxId: w.TxID.String(),
|
||||
TxId: txID,
|
||||
Deposits: deposits,
|
||||
TotalDepositAmountSatoshis: int64(w.TotalDepositAmount),
|
||||
WithdrawnAmountSatoshis: int64(w.WithdrawnAmount),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- withdrawals stores finalized static address withdrawals.
|
||||
-- withdrawals stores pending and finalized static address withdrawals.
|
||||
CREATE TABLE IF NOT EXISTS withdrawals (
|
||||
-- id is the auto-incrementing primary key for a withdrawal.
|
||||
id INTEGER PRIMARY KEY,
|
||||
|
|
@ -6,7 +6,8 @@ CREATE TABLE IF NOT EXISTS withdrawals (
|
|||
-- 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 is the confirmed transaction txid of the withdrawal.
|
||||
-- It remains NULL while the withdrawal is still pending.
|
||||
withdrawal_tx_id TEXT UNIQUE,
|
||||
|
||||
-- total_deposit_amount is the total amount of the deposits in satoshis.
|
||||
|
|
|
|||
|
|
@ -5695,7 +5695,8 @@ func (x *Deposit) GetSwapHash() []byte {
|
|||
|
||||
type StaticAddressWithdrawal struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// The transaction id of the withdrawal transaction.
|
||||
// The transaction id of the withdrawal transaction. It is empty until the
|
||||
// confirmed transaction is persisted.
|
||||
TxId string `protobuf:"bytes,1,opt,name=tx_id,json=txId,proto3" json:"tx_id,omitempty"`
|
||||
// The selected deposits that is withdrawn from.
|
||||
Deposits []*Deposit `protobuf:"bytes,2,rep,name=deposits,proto3" json:"deposits,omitempty"`
|
||||
|
|
@ -5703,11 +5704,13 @@ type StaticAddressWithdrawal struct {
|
|||
TotalDepositAmountSatoshis int64 `protobuf:"varint,3,opt,name=total_deposit_amount_satoshis,json=totalDepositAmountSatoshis,proto3" json:"total_deposit_amount_satoshis,omitempty"`
|
||||
// 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.
|
||||
// change output. It is zero until the confirmed transaction is persisted.
|
||||
WithdrawnAmountSatoshis int64 `protobuf:"varint,4,opt,name=withdrawn_amount_satoshis,json=withdrawnAmountSatoshis,proto3" json:"withdrawn_amount_satoshis,omitempty"`
|
||||
// An optional change.
|
||||
// An optional change. It is zero until the confirmed transaction is
|
||||
// persisted.
|
||||
ChangeAmountSatoshis int64 `protobuf:"varint,5,opt,name=change_amount_satoshis,json=changeAmountSatoshis,proto3" json:"change_amount_satoshis,omitempty"`
|
||||
// The confirmation block height of the withdrawal transaction.
|
||||
// The confirmation block height of the withdrawal transaction. It is zero
|
||||
// until the withdrawal is confirmed.
|
||||
ConfirmationHeight uint32 `protobuf:"varint,6,opt,name=confirmation_height,json=confirmationHeight,proto3" json:"confirmation_height,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
|
|
|
|||
|
|
@ -188,7 +188,8 @@ service SwapClient {
|
|||
returns (ListStaticAddressDepositsResponse);
|
||||
|
||||
/* loop:`listwithdrawals`
|
||||
ListStaticAddressWithdrawals returns a list of static address withdrawals.
|
||||
ListStaticAddressWithdrawals returns a list of static address withdrawals,
|
||||
including pending withdrawals that have not yet been confirmed.
|
||||
*/
|
||||
rpc ListStaticAddressWithdrawals (ListStaticAddressWithdrawalRequest)
|
||||
returns (ListStaticAddressWithdrawalResponse);
|
||||
|
|
@ -2047,7 +2048,8 @@ message Deposit {
|
|||
|
||||
message StaticAddressWithdrawal {
|
||||
/*
|
||||
The transaction id of the withdrawal transaction.
|
||||
The transaction id of the withdrawal transaction. It is empty until the
|
||||
confirmed transaction is persisted.
|
||||
*/
|
||||
string tx_id = 1;
|
||||
|
||||
|
|
@ -2064,17 +2066,19 @@ message StaticAddressWithdrawal {
|
|||
/*
|
||||
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.
|
||||
change output. It is zero until the confirmed transaction is persisted.
|
||||
*/
|
||||
int64 withdrawn_amount_satoshis = 4;
|
||||
|
||||
/*
|
||||
An optional change.
|
||||
An optional change. It is zero until the confirmed transaction is
|
||||
persisted.
|
||||
*/
|
||||
int64 change_amount_satoshis = 5;
|
||||
|
||||
/*
|
||||
The confirmation block height of the withdrawal transaction.
|
||||
The confirmation block height of the withdrawal transaction. It is zero
|
||||
until the withdrawal is confirmed.
|
||||
*/
|
||||
uint32 confirmation_height = 6;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1115,7 +1115,7 @@
|
|||
},
|
||||
"/v1/staticaddr/withdrawals": {
|
||||
"get": {
|
||||
"summary": "loop:`listwithdrawals`\nListStaticAddressWithdrawals returns a list of static address withdrawals.",
|
||||
"summary": "loop:`listwithdrawals`\nListStaticAddressWithdrawals returns a list of static address withdrawals,\nincluding pending withdrawals that have not yet been confirmed.",
|
||||
"operationId": "SwapClient_ListStaticAddressWithdrawals",
|
||||
"responses": {
|
||||
"200": {
|
||||
|
|
@ -2872,7 +2872,7 @@
|
|||
"properties": {
|
||||
"tx_id": {
|
||||
"type": "string",
|
||||
"description": "The transaction id of the withdrawal transaction."
|
||||
"description": "The transaction id of the withdrawal transaction. It is empty until the\nconfirmed transaction is persisted."
|
||||
},
|
||||
"deposits": {
|
||||
"type": "array",
|
||||
|
|
@ -2890,17 +2890,17 @@
|
|||
"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."
|
||||
"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. It is zero until the confirmed transaction is persisted."
|
||||
},
|
||||
"change_amount_satoshis": {
|
||||
"type": "string",
|
||||
"format": "int64",
|
||||
"description": "An optional change."
|
||||
"description": "An optional change. It is zero until the confirmed transaction is\npersisted."
|
||||
},
|
||||
"confirmation_height": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The confirmation block height of the withdrawal transaction."
|
||||
"description": "The confirmation block height of the withdrawal transaction. It is zero\nuntil the withdrawal is confirmed."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -127,7 +127,8 @@ type SwapClientClient interface {
|
|||
// deposits.
|
||||
ListStaticAddressDeposits(ctx context.Context, in *ListStaticAddressDepositsRequest, opts ...grpc.CallOption) (*ListStaticAddressDepositsResponse, error)
|
||||
// loop:`listwithdrawals`
|
||||
// ListStaticAddressWithdrawals returns a list of static address withdrawals.
|
||||
// ListStaticAddressWithdrawals returns a list of static address withdrawals,
|
||||
// including pending withdrawals that have not yet been confirmed.
|
||||
ListStaticAddressWithdrawals(ctx context.Context, in *ListStaticAddressWithdrawalRequest, opts ...grpc.CallOption) (*ListStaticAddressWithdrawalResponse, error)
|
||||
// loop:`listswaps`
|
||||
// ListStaticAddressSwaps returns a list of filtered static address
|
||||
|
|
@ -587,7 +588,8 @@ type SwapClientServer interface {
|
|||
// deposits.
|
||||
ListStaticAddressDeposits(context.Context, *ListStaticAddressDepositsRequest) (*ListStaticAddressDepositsResponse, error)
|
||||
// loop:`listwithdrawals`
|
||||
// ListStaticAddressWithdrawals returns a list of static address withdrawals.
|
||||
// ListStaticAddressWithdrawals returns a list of static address withdrawals,
|
||||
// including pending withdrawals that have not yet been confirmed.
|
||||
ListStaticAddressWithdrawals(context.Context, *ListStaticAddressWithdrawalRequest) (*ListStaticAddressWithdrawalResponse, error)
|
||||
// loop:`listswaps`
|
||||
// ListStaticAddressSwaps returns a list of filtered static address
|
||||
|
|
|
|||
|
|
@ -92,8 +92,8 @@ 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 is the store that is used to persist pending and finalized
|
||||
// withdrawal records.
|
||||
Store *SqlStore
|
||||
}
|
||||
|
||||
|
|
@ -1187,7 +1187,8 @@ func (m *Manager) DeliverWithdrawalRequest(ctx context.Context,
|
|||
}
|
||||
}
|
||||
|
||||
// GetAllWithdrawals returns all finalized withdrawals from the store.
|
||||
// GetAllWithdrawals returns all pending and finalized withdrawals from the
|
||||
// store.
|
||||
func (m *Manager) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) {
|
||||
return m.cfg.Store.GetAllWithdrawals(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
|
@ -37,7 +39,8 @@ type Querier interface {
|
|||
GetWithdrawalDeposits(ctx context.Context, withdrawalID []byte) (
|
||||
[][]byte, error)
|
||||
|
||||
// GetAllWithdrawals retrieves all withdrawals from the database.
|
||||
// GetAllWithdrawals retrieves all pending and finalized withdrawals from
|
||||
// the database.
|
||||
GetAllWithdrawals(ctx context.Context) ([]sqlc.Withdrawal, error)
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +72,8 @@ func NewSqlStore(db BaseDB, depositStore deposit.Store) *SqlStore {
|
|||
}
|
||||
}
|
||||
|
||||
// CreateWithdrawal creates a static address withdrawal record in the database.
|
||||
// CreateWithdrawal creates a pending static address withdrawal record in the
|
||||
// database.
|
||||
func (s *SqlStore) CreateWithdrawal(ctx context.Context,
|
||||
deposits []*deposit.Deposit) error {
|
||||
|
||||
|
|
@ -110,8 +114,8 @@ func (s *SqlStore) CreateWithdrawal(ctx context.Context,
|
|||
})
|
||||
}
|
||||
|
||||
// UpdateWithdrawal updates a withdrawal record with the transaction
|
||||
// information, including the withdrawn amount, change amount, and
|
||||
// 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.
|
||||
|
|
@ -169,9 +173,9 @@ func (s *SqlStore) UpdateWithdrawal(ctx context.Context,
|
|||
})
|
||||
}
|
||||
|
||||
// GetAllWithdrawals retrieves all static address withdrawals from the
|
||||
// database. It returns a slice of Withdrawal structs, each containing a list
|
||||
// of associated deposits.
|
||||
// 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) {
|
||||
|
||||
|
|
@ -200,14 +204,22 @@ func (s *SqlStore) GetAllWithdrawals(ctx context.Context) ([]Withdrawal,
|
|||
deposits = append(deposits, deposit)
|
||||
}
|
||||
|
||||
txID, err := chainhash.NewHashFromStr(w.WithdrawalTxID.String)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
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,
|
||||
TxID: txID,
|
||||
Deposits: deposits,
|
||||
TotalDepositAmount: btcutil.Amount(w.TotalDepositAmount),
|
||||
WithdrawnAmount: btcutil.Amount(w.WithdrawnAmount.Int64),
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ package withdraw
|
|||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightninglabs/loop/loopdb"
|
||||
"github.com/lightninglabs/loop/loopdb/sqlc"
|
||||
"github.com/lightninglabs/loop/staticaddr/deposit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -83,6 +85,10 @@ func TestSqlStore(t *testing.T) {
|
|||
t, d2.Value, withdrawals[0].Deposits[1].Value,
|
||||
)
|
||||
require.NotEmpty(t, withdrawals[0].InitiationTime)
|
||||
require.Nil(t, withdrawals[0].TxID)
|
||||
require.Zero(t, withdrawals[0].WithdrawnAmount)
|
||||
require.Zero(t, withdrawals[0].ChangeAmount)
|
||||
require.Zero(t, withdrawals[0].ConfirmationHeight)
|
||||
|
||||
err = store.UpdateWithdrawal(
|
||||
ctxb, []*deposit.Deposit{d1, d2}, withdrawalTx, 6, []byte{0x01},
|
||||
|
|
@ -92,10 +98,57 @@ func TestSqlStore(t *testing.T) {
|
|||
withdrawals, err = store.GetAllWithdrawals(ctxb)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, withdrawals, 1)
|
||||
require.NotEmpty(t, withdrawals[0].TxID)
|
||||
require.NotNil(t, withdrawals[0].TxID)
|
||||
require.Equal(t, withdrawalTx.TxHash(), *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)
|
||||
}
|
||||
|
||||
// TestGetAllWithdrawalsRejectsInvalidTxID verifies that a malformed persisted
|
||||
// withdrawal txid is rejected, while pending withdrawals remain readable via
|
||||
// NULL values.
|
||||
func TestGetAllWithdrawalsRejectsInvalidTxID(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)
|
||||
|
||||
depositID, err := deposit.GetRandomDepositID()
|
||||
require.NoError(t, err)
|
||||
|
||||
d := &deposit.Deposit{
|
||||
ID: depositID,
|
||||
Value: btcutil.Amount(100_000),
|
||||
TimeOutSweepPkScript: []byte{
|
||||
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
|
||||
},
|
||||
}
|
||||
|
||||
err = depositStore.CreateDeposit(ctxb, d)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = store.CreateWithdrawal(ctxb, []*deposit.Deposit{d})
|
||||
require.NoError(t, err)
|
||||
|
||||
withdrawalID, err := testDb.Queries.GetWithdrawalIDByDepositID(
|
||||
ctxb, d.ID[:],
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = testDb.Queries.UpdateWithdrawal(ctxb, sqlc.UpdateWithdrawalParams{
|
||||
WithdrawalID: withdrawalID,
|
||||
WithdrawalTxID: sql.NullString{
|
||||
String: "abcd",
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.GetAllWithdrawals(ctxb)
|
||||
require.ErrorContains(t, err, "invalid withdrawal txid")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,14 +29,15 @@ func (r *ID) FromByteSlice(b []byte) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Withdrawal represents a finalized static address withdrawal record in the
|
||||
// database.
|
||||
// Withdrawal represents a static address withdrawal record in the database.
|
||||
// The record may be pending or finalized.
|
||||
type Withdrawal struct {
|
||||
// ID is the unique identifier of the deposit.
|
||||
ID ID
|
||||
|
||||
// TxID is the transaction ID of the withdrawal.
|
||||
TxID chainhash.Hash
|
||||
// TxID is the transaction ID of the withdrawal. It is nil until the
|
||||
// confirmed withdrawal transaction is persisted.
|
||||
TxID *chainhash.Hash
|
||||
|
||||
// Deposits is a list of deposits used to fund the withdrawal.
|
||||
Deposits []*deposit.Deposit
|
||||
|
|
@ -46,17 +47,19 @@ type Withdrawal struct {
|
|||
TotalDepositAmount btcutil.Amount
|
||||
|
||||
// WithdrawnAmount is the amount withdrawn. It represents the total
|
||||
// value of selected deposits minus fees and change.
|
||||
// value of selected deposits minus fees and change. It is zero until the
|
||||
// confirmed withdrawal transaction is persisted.
|
||||
WithdrawnAmount btcutil.Amount
|
||||
|
||||
// ChangeAmount is the optional change returned to the static address.
|
||||
// ChangeAmount is the optional change returned to the static address. It
|
||||
// is zero until the confirmed withdrawal transaction is persisted.
|
||||
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.
|
||||
// confirmed. It is zero until the withdrawal is confirmed.
|
||||
ConfirmationHeight int64
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue