Skip to content

Commit bfa83b9

Browse files
committed
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.
1 parent 7364277 commit bfa83b9

File tree

10 files changed

+129
-43
lines changed

10 files changed

+129
-43
lines changed

loopd/swapclient_server.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,8 +1836,10 @@ func (s *swapClientServer) ListStaticAddressDeposits(ctx context.Context,
18361836
}, nil
18371837
}
18381838

1839-
// ListStaticAddressWithdrawals returns a list of all finalized withdrawal
1840-
// transactions.
1839+
// ListStaticAddressWithdrawals returns a list of all static address
1840+
// withdrawals, including pending withdrawals. Pending withdrawals expose
1841+
// default empty or zero values for fields that are only known after
1842+
// confirmation.
18411843
func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
18421844
_ *looprpc.ListStaticAddressWithdrawalRequest) (
18431845
*looprpc.ListStaticAddressWithdrawalResponse, error) {
@@ -1855,6 +1857,11 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
18551857
[]*looprpc.StaticAddressWithdrawal, 0, len(withdrawals),
18561858
)
18571859
for _, w := range withdrawals {
1860+
txID := ""
1861+
if w.TxID != nil {
1862+
txID = w.TxID.String()
1863+
}
1864+
18581865
deposits := make([]*looprpc.Deposit, 0, len(w.Deposits))
18591866
for _, d := range w.Deposits {
18601867
deposits = append(deposits, &looprpc.Deposit{
@@ -1868,7 +1875,7 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context,
18681875
})
18691876
}
18701877
withdrawal := &looprpc.StaticAddressWithdrawal{
1871-
TxId: w.TxID.String(),
1878+
TxId: txID,
18721879
Deposits: deposits,
18731880
TotalDepositAmountSatoshis: int64(w.TotalDepositAmount),
18741881
WithdrawnAmountSatoshis: int64(w.WithdrawnAmount),

loopdb/sqlc/migrations/000015_static_address_withdrawals.up.sql

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
-- withdrawals stores finalized static address withdrawals.
1+
-- withdrawals stores pending and finalized static address withdrawals.
22
CREATE TABLE IF NOT EXISTS withdrawals (
33
-- id is the auto-incrementing primary key for a withdrawal.
44
id INTEGER PRIMARY KEY,
55

66
-- withdrawal_id is the unique identifier for the withdrawal.
77
withdrawal_id BLOB NOT NULL UNIQUE,
88

9-
-- withdrawal_tx_id is the transaction tx id of the withdrawal.
9+
-- withdrawal_tx_id is the confirmed transaction txid of the withdrawal.
10+
-- It remains NULL while the withdrawal is still pending.
1011
withdrawal_tx_id TEXT UNIQUE,
1112

1213
-- total_deposit_amount is the total amount of the deposits in satoshis.

looprpc/client.pb.go

Lines changed: 7 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

looprpc/client.proto

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,8 @@ service SwapClient {
188188
returns (ListStaticAddressDepositsResponse);
189189

190190
/* loop:`listwithdrawals`
191-
ListStaticAddressWithdrawals returns a list of static address withdrawals.
191+
ListStaticAddressWithdrawals returns a list of static address withdrawals,
192+
including pending withdrawals that have not yet been confirmed.
192193
*/
193194
rpc ListStaticAddressWithdrawals (ListStaticAddressWithdrawalRequest)
194195
returns (ListStaticAddressWithdrawalResponse);
@@ -2047,7 +2048,8 @@ message Deposit {
20472048

20482049
message StaticAddressWithdrawal {
20492050
/*
2050-
The transaction id of the withdrawal transaction.
2051+
The transaction id of the withdrawal transaction. It is empty until the
2052+
confirmed transaction is persisted.
20512053
*/
20522054
string tx_id = 1;
20532055

@@ -2064,17 +2066,19 @@ message StaticAddressWithdrawal {
20642066
/*
20652067
The actual amount that was withdrawn from the selected deposits. This value
20662068
represents the sum of selected deposit values minus tx fees minus optional
2067-
change output.
2069+
change output. It is zero until the confirmed transaction is persisted.
20682070
*/
20692071
int64 withdrawn_amount_satoshis = 4;
20702072

20712073
/*
2072-
An optional change.
2074+
An optional change. It is zero until the confirmed transaction is
2075+
persisted.
20732076
*/
20742077
int64 change_amount_satoshis = 5;
20752078

20762079
/*
2077-
The confirmation block height of the withdrawal transaction.
2080+
The confirmation block height of the withdrawal transaction. It is zero
2081+
until the withdrawal is confirmed.
20782082
*/
20792083
uint32 confirmation_height = 6;
20802084
}

looprpc/client.swagger.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,7 @@
11151115
},
11161116
"/v1/staticaddr/withdrawals": {
11171117
"get": {
1118-
"summary": "loop:`listwithdrawals`\nListStaticAddressWithdrawals returns a list of static address withdrawals.",
1118+
"summary": "loop:`listwithdrawals`\nListStaticAddressWithdrawals returns a list of static address withdrawals,\nincluding pending withdrawals that have not yet been confirmed.",
11191119
"operationId": "SwapClient_ListStaticAddressWithdrawals",
11201120
"responses": {
11211121
"200": {
@@ -2872,7 +2872,7 @@
28722872
"properties": {
28732873
"tx_id": {
28742874
"type": "string",
2875-
"description": "The transaction id of the withdrawal transaction."
2875+
"description": "The transaction id of the withdrawal transaction. It is empty until the\nconfirmed transaction is persisted."
28762876
},
28772877
"deposits": {
28782878
"type": "array",
@@ -2890,17 +2890,17 @@
28902890
"withdrawn_amount_satoshis": {
28912891
"type": "string",
28922892
"format": "int64",
2893-
"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."
2893+
"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."
28942894
},
28952895
"change_amount_satoshis": {
28962896
"type": "string",
28972897
"format": "int64",
2898-
"description": "An optional change."
2898+
"description": "An optional change. It is zero until the confirmed transaction is\npersisted."
28992899
},
29002900
"confirmation_height": {
29012901
"type": "integer",
29022902
"format": "int64",
2903-
"description": "The confirmation block height of the withdrawal transaction."
2903+
"description": "The confirmation block height of the withdrawal transaction. It is zero\nuntil the withdrawal is confirmed."
29042904
}
29052905
}
29062906
},

looprpc/client_grpc.pb.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

staticaddr/withdraw/manager.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ type ManagerConfig struct {
9292
// Signer is the signer client that is used to sign transactions.
9393
Signer lndclient.SignerClient
9494

95-
// Store is the store that is used to persist the finalized withdrawal
96-
// transactions.
95+
// Store is the store that is used to persist pending and finalized
96+
// withdrawal records.
9797
Store *SqlStore
9898
}
9999

@@ -1187,7 +1187,8 @@ func (m *Manager) DeliverWithdrawalRequest(ctx context.Context,
11871187
}
11881188
}
11891189

1190-
// GetAllWithdrawals returns all finalized withdrawals from the store.
1190+
// GetAllWithdrawals returns all pending and finalized withdrawals from the
1191+
// store.
11911192
func (m *Manager) GetAllWithdrawals(ctx context.Context) ([]Withdrawal, error) {
11921193
return m.cfg.Store.GetAllWithdrawals(ctx)
11931194
}

staticaddr/withdraw/sql_store.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import (
44
"bytes"
55
"context"
66
"database/sql"
7+
"fmt"
78

89
"github.com/btcsuite/btcd/btcutil"
910
"github.com/btcsuite/btcd/chaincfg/chainhash"
1011
"github.com/btcsuite/btcd/wire"
1112
"github.com/lightninglabs/loop/loopdb"
1213
"github.com/lightninglabs/loop/loopdb/sqlc"
1314
"github.com/lightninglabs/loop/staticaddr/deposit"
15+
"github.com/lightninglabs/loop/utils/chainhashutil"
1416
"github.com/lightningnetwork/lnd/clock"
1517
)
1618

@@ -37,7 +39,8 @@ type Querier interface {
3739
GetWithdrawalDeposits(ctx context.Context, withdrawalID []byte) (
3840
[][]byte, error)
3941

40-
// GetAllWithdrawals retrieves all withdrawals from the database.
42+
// GetAllWithdrawals retrieves all pending and finalized withdrawals from
43+
// the database.
4144
GetAllWithdrawals(ctx context.Context) ([]sqlc.Withdrawal, error)
4245
}
4346

@@ -69,7 +72,8 @@ func NewSqlStore(db BaseDB, depositStore deposit.Store) *SqlStore {
6972
}
7073
}
7174

72-
// CreateWithdrawal creates a static address withdrawal record in the database.
75+
// CreateWithdrawal creates a pending static address withdrawal record in the
76+
// database.
7377
func (s *SqlStore) CreateWithdrawal(ctx context.Context,
7478
deposits []*deposit.Deposit) error {
7579

@@ -110,8 +114,8 @@ func (s *SqlStore) CreateWithdrawal(ctx context.Context,
110114
})
111115
}
112116

113-
// UpdateWithdrawal updates a withdrawal record with the transaction
114-
// information, including the withdrawn amount, change amount, and
117+
// UpdateWithdrawal finalizes a pending withdrawal record with the confirmed
118+
// transaction information, including the withdrawn amount, change amount, and
115119
// confirmation height. It is expected that the withdrawal has already been
116120
// created with CreateWithdrawal, and that the deposits slice contains the
117121
// deposits associated with the withdrawal.
@@ -169,9 +173,9 @@ func (s *SqlStore) UpdateWithdrawal(ctx context.Context,
169173
})
170174
}
171175

172-
// GetAllWithdrawals retrieves all static address withdrawals from the
173-
// database. It returns a slice of Withdrawal structs, each containing a list
174-
// of associated deposits.
176+
// GetAllWithdrawals retrieves all pending and finalized static address
177+
// withdrawals from the database. Pending withdrawals return default zero
178+
// values for fields that are only known after confirmation, and a nil TxID.
175179
func (s *SqlStore) GetAllWithdrawals(ctx context.Context) ([]Withdrawal,
176180
error) {
177181

@@ -200,14 +204,22 @@ func (s *SqlStore) GetAllWithdrawals(ctx context.Context) ([]Withdrawal,
200204
deposits = append(deposits, deposit)
201205
}
202206

203-
txID, err := chainhash.NewHashFromStr(w.WithdrawalTxID.String)
204-
if err != nil {
205-
return nil, err
207+
var txID *chainhash.Hash
208+
if w.WithdrawalTxID.Valid {
209+
hash, err := chainhashutil.NewHashFromStrExact(
210+
w.WithdrawalTxID.String,
211+
)
212+
if err != nil {
213+
return nil, fmt.Errorf("invalid withdrawal txid %q: %w",
214+
w.WithdrawalTxID.String, err)
215+
}
216+
217+
txID = &hash
206218
}
207219

208220
result = append(result, Withdrawal{
209221
ID: ID(w.WithdrawalID),
210-
TxID: *txID,
222+
TxID: txID,
211223
Deposits: deposits,
212224
TotalDepositAmount: btcutil.Amount(w.TotalDepositAmount),
213225
WithdrawnAmount: btcutil.Amount(w.WithdrawnAmount.Int64),

staticaddr/withdraw/sql_store_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package withdraw
22

33
import (
44
"context"
5+
"database/sql"
56
"testing"
67

78
"github.com/btcsuite/btcd/btcutil"
89
"github.com/btcsuite/btcd/wire"
910
"github.com/lightninglabs/loop/loopdb"
11+
"github.com/lightninglabs/loop/loopdb/sqlc"
1012
"github.com/lightninglabs/loop/staticaddr/deposit"
1113
"github.com/stretchr/testify/require"
1214
)
@@ -83,6 +85,10 @@ func TestSqlStore(t *testing.T) {
8385
t, d2.Value, withdrawals[0].Deposits[1].Value,
8486
)
8587
require.NotEmpty(t, withdrawals[0].InitiationTime)
88+
require.Nil(t, withdrawals[0].TxID)
89+
require.Zero(t, withdrawals[0].WithdrawnAmount)
90+
require.Zero(t, withdrawals[0].ChangeAmount)
91+
require.Zero(t, withdrawals[0].ConfirmationHeight)
8692

8793
err = store.UpdateWithdrawal(
8894
ctxb, []*deposit.Deposit{d1, d2}, withdrawalTx, 6, []byte{0x01},
@@ -92,10 +98,57 @@ func TestSqlStore(t *testing.T) {
9298
withdrawals, err = store.GetAllWithdrawals(ctxb)
9399
require.NoError(t, err)
94100
require.Len(t, withdrawals, 1)
95-
require.NotEmpty(t, withdrawals[0].TxID)
101+
require.NotNil(t, withdrawals[0].TxID)
102+
require.Equal(t, withdrawalTx.TxHash(), *withdrawals[0].TxID)
96103
require.EqualValues(
97104
t, d1.Value+d2.Value-100, withdrawals[0].WithdrawnAmount,
98105
)
99106
require.EqualValues(t, 100, withdrawals[0].ChangeAmount)
100107
require.EqualValues(t, 6, withdrawals[0].ConfirmationHeight)
101108
}
109+
110+
// TestGetAllWithdrawalsRejectsInvalidTxID verifies that a malformed persisted
111+
// withdrawal txid is rejected, while pending withdrawals remain readable via
112+
// NULL values.
113+
func TestGetAllWithdrawalsRejectsInvalidTxID(t *testing.T) {
114+
ctxb := context.Background()
115+
testDb := loopdb.NewTestDB(t)
116+
defer testDb.Close()
117+
118+
depositStore := deposit.NewSqlStore(testDb.BaseDB)
119+
store := NewSqlStore(loopdb.NewTypedStore[Querier](testDb), depositStore)
120+
121+
depositID, err := deposit.GetRandomDepositID()
122+
require.NoError(t, err)
123+
124+
d := &deposit.Deposit{
125+
ID: depositID,
126+
Value: btcutil.Amount(100_000),
127+
TimeOutSweepPkScript: []byte{
128+
0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41,
129+
},
130+
}
131+
132+
err = depositStore.CreateDeposit(ctxb, d)
133+
require.NoError(t, err)
134+
135+
err = store.CreateWithdrawal(ctxb, []*deposit.Deposit{d})
136+
require.NoError(t, err)
137+
138+
withdrawalID, err := testDb.Queries.GetWithdrawalIDByDepositID(
139+
ctxb, d.ID[:],
140+
)
141+
require.NoError(t, err)
142+
143+
err = testDb.Queries.UpdateWithdrawal(ctxb, sqlc.UpdateWithdrawalParams{
144+
WithdrawalID: withdrawalID,
145+
WithdrawalTxID: sql.NullString{
146+
String: "abcd",
147+
Valid: true,
148+
},
149+
})
150+
require.NoError(t, err)
151+
152+
_, err = store.GetAllWithdrawals(ctxb)
153+
require.ErrorContains(t, err, "invalid withdrawal txid")
154+
}

0 commit comments

Comments
 (0)