Skip to content

Commit 60df5fe

Browse files
committed
reservations: add reservation sql store
1 parent a29f7e4 commit 60df5fe

File tree

2 files changed

+394
-0
lines changed

2 files changed

+394
-0
lines changed

instantout/reservation/store.go

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package reservation
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
8+
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/btcsuite/btcd/btcutil"
10+
"github.com/btcsuite/btcd/chaincfg/chainhash"
11+
"github.com/btcsuite/btcd/wire"
12+
"github.com/lightninglabs/loop/fsm"
13+
"github.com/lightninglabs/loop/loopdb"
14+
"github.com/lightninglabs/loop/loopdb/sqlc"
15+
"github.com/lightningnetwork/lnd/clock"
16+
"github.com/lightningnetwork/lnd/keychain"
17+
)
18+
19+
// BaseDB is the interface that contains all the queries generated
20+
// by sqlc for the reservation table.
21+
type BaseDB interface {
22+
// CreateReservation stores the reservation in the database.
23+
CreateReservation(ctx context.Context,
24+
arg sqlc.CreateReservationParams) error
25+
26+
// GetReservation retrieves the reservation from the database.
27+
GetReservation(ctx context.Context,
28+
reservationID []byte) (sqlc.Reservation, error)
29+
30+
// GetReservationUpdates fetches all updates for a reservation.
31+
GetReservationUpdates(ctx context.Context,
32+
reservationID []byte) ([]sqlc.ReservationUpdate, error)
33+
34+
// GetReservations lists all existing reservations the client has ever
35+
// made.
36+
GetReservations(ctx context.Context) ([]sqlc.Reservation, error)
37+
38+
// UpdateReservation inserts a new reservation update.
39+
UpdateReservation(ctx context.Context,
40+
arg sqlc.UpdateReservationParams) error
41+
42+
// ExecTx allows for executing a function in the context of a database
43+
// transaction.
44+
ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
45+
txBody func(*sqlc.Queries) error) error
46+
}
47+
48+
// SQLStore manages the reservations in the database.
49+
type SQLStore struct {
50+
baseDb BaseDB
51+
52+
clock clock.Clock
53+
}
54+
55+
// NewSQLStore creates a new SQLStore.
56+
func NewSQLStore(db BaseDB) *SQLStore {
57+
return &SQLStore{
58+
baseDb: db,
59+
clock: clock.NewDefaultClock(),
60+
}
61+
}
62+
63+
// CreateReservation stores the reservation in the database.
64+
func (r *SQLStore) CreateReservation(ctx context.Context,
65+
reservation *Reservation) error {
66+
67+
args := sqlc.CreateReservationParams{
68+
ReservationID: reservation.ID[:],
69+
ClientPubkey: reservation.ClientPubkey.SerializeCompressed(),
70+
ServerPubkey: reservation.ServerPubkey.SerializeCompressed(),
71+
Expiry: int32(reservation.Expiry),
72+
Value: int64(reservation.Value),
73+
ClientKeyFamily: int32(reservation.KeyLocator.Family),
74+
ClientKeyIndex: int32(reservation.KeyLocator.Index),
75+
InitiationHeight: reservation.InitiationHeight,
76+
}
77+
78+
updateArgs := sqlc.InsertReservationUpdateParams{
79+
ReservationID: reservation.ID[:],
80+
UpdateTimestamp: r.clock.Now().UTC(),
81+
UpdateState: string(reservation.State),
82+
}
83+
84+
return r.baseDb.ExecTx(ctx, &loopdb.SqliteTxOptions{},
85+
func(q *sqlc.Queries) error {
86+
err := q.CreateReservation(ctx, args)
87+
if err != nil {
88+
return err
89+
}
90+
91+
return q.InsertReservationUpdate(ctx, updateArgs)
92+
})
93+
}
94+
95+
// UpdateReservation updates the reservation in the database.
96+
func (r *SQLStore) UpdateReservation(ctx context.Context,
97+
reservation *Reservation) error {
98+
99+
var txHash []byte
100+
var outIndex sql.NullInt32
101+
if reservation.Outpoint != nil {
102+
txHash = reservation.Outpoint.Hash[:]
103+
outIndex = sql.NullInt32{
104+
Int32: int32(reservation.Outpoint.Index),
105+
Valid: true,
106+
}
107+
}
108+
109+
insertUpdateArgs := sqlc.InsertReservationUpdateParams{
110+
ReservationID: reservation.ID[:],
111+
UpdateTimestamp: r.clock.Now().UTC(),
112+
UpdateState: string(reservation.State),
113+
}
114+
115+
updateArgs := sqlc.UpdateReservationParams{
116+
ReservationID: reservation.ID[:],
117+
TxHash: txHash,
118+
OutIndex: outIndex,
119+
ConfirmationHeight: marshalSqlNullInt32(
120+
int32(reservation.ConfirmationHeight),
121+
),
122+
}
123+
124+
return r.baseDb.ExecTx(ctx, &loopdb.SqliteTxOptions{},
125+
func(q *sqlc.Queries) error {
126+
err := q.UpdateReservation(ctx, updateArgs)
127+
if err != nil {
128+
return err
129+
}
130+
131+
return q.InsertReservationUpdate(ctx, insertUpdateArgs)
132+
})
133+
}
134+
135+
// GetReservation retrieves the reservation from the database.
136+
func (r *SQLStore) GetReservation(ctx context.Context,
137+
reservationId ID) (*Reservation, error) {
138+
139+
var reservation *Reservation
140+
err := r.baseDb.ExecTx(ctx, loopdb.NewSqlReadOpts(),
141+
func(q *sqlc.Queries) error {
142+
var err error
143+
reservationRow, err := q.GetReservation(
144+
ctx, reservationId[:],
145+
)
146+
if err != nil {
147+
return err
148+
}
149+
150+
reservationUpdates, err := q.GetReservationUpdates(
151+
ctx, reservationId[:],
152+
)
153+
if err != nil {
154+
return err
155+
}
156+
157+
if len(reservationUpdates) == 0 {
158+
return errors.New("no reservation updates")
159+
}
160+
161+
reservation, err = sqlReservationToReservation(
162+
reservationRow,
163+
reservationUpdates[len(reservationUpdates)-1],
164+
)
165+
if err != nil {
166+
return err
167+
}
168+
169+
return nil
170+
})
171+
if err != nil {
172+
return nil, err
173+
}
174+
175+
return reservation, nil
176+
}
177+
178+
// ListReservations lists all existing reservations the client has ever made.
179+
func (r *SQLStore) ListReservations(ctx context.Context) ([]*Reservation,
180+
error) {
181+
182+
var result []*Reservation
183+
184+
err := r.baseDb.ExecTx(ctx, loopdb.NewSqlReadOpts(),
185+
func(q *sqlc.Queries) error {
186+
var err error
187+
188+
reservations, err := q.GetReservations(ctx)
189+
if err != nil {
190+
return err
191+
}
192+
193+
for _, reservation := range reservations {
194+
reservationUpdates, err := q.GetReservationUpdates(
195+
ctx, reservation.ReservationID,
196+
)
197+
if err != nil {
198+
return err
199+
}
200+
201+
if len(reservationUpdates) == 0 {
202+
return errors.New(
203+
"no reservation updates",
204+
)
205+
}
206+
207+
res, err := sqlReservationToReservation(
208+
reservation, reservationUpdates[len(
209+
reservationUpdates,
210+
)-1],
211+
)
212+
if err != nil {
213+
return err
214+
}
215+
216+
result = append(result, res)
217+
}
218+
219+
return nil
220+
})
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
return result, nil
226+
}
227+
228+
// sqlReservationToReservation converts a sql reservation to a reservation.
229+
func sqlReservationToReservation(row sqlc.Reservation,
230+
lastUpdate sqlc.ReservationUpdate) (*Reservation,
231+
error) {
232+
233+
id := ID{}
234+
err := id.FromByteSlice(row.ReservationID)
235+
if err != nil {
236+
return nil, err
237+
}
238+
239+
clientPubkey, err := btcec.ParsePubKey(row.ClientPubkey)
240+
if err != nil {
241+
return nil, err
242+
}
243+
244+
serverPubkey, err := btcec.ParsePubKey(row.ServerPubkey)
245+
if err != nil {
246+
return nil, err
247+
}
248+
249+
var txHash *chainhash.Hash
250+
if row.TxHash != nil {
251+
txHash, err = chainhash.NewHash(row.TxHash)
252+
if err != nil {
253+
return nil, err
254+
}
255+
}
256+
257+
var outpoint *wire.OutPoint
258+
if row.OutIndex.Valid {
259+
outpoint = wire.NewOutPoint(
260+
txHash, uint32(unmarshalSqlNullInt32(row.OutIndex)),
261+
)
262+
}
263+
264+
return &Reservation{
265+
ID: id,
266+
ClientPubkey: clientPubkey,
267+
ServerPubkey: serverPubkey,
268+
Expiry: uint32(row.Expiry),
269+
Value: btcutil.Amount(row.Value),
270+
KeyLocator: keychain.KeyLocator{
271+
Family: keychain.KeyFamily(row.ClientKeyFamily),
272+
Index: uint32(row.ClientKeyIndex),
273+
},
274+
Outpoint: outpoint,
275+
ConfirmationHeight: uint32(
276+
unmarshalSqlNullInt32(row.ConfirmationHeight),
277+
),
278+
InitiationHeight: row.InitiationHeight,
279+
State: fsm.StateType(lastUpdate.UpdateState),
280+
}, nil
281+
}
282+
283+
// marshalSqlNullInt32 converts an int32 to a sql.NullInt32.
284+
func marshalSqlNullInt32(i int32) sql.NullInt32 {
285+
return sql.NullInt32{
286+
Int32: i,
287+
Valid: i != 0,
288+
}
289+
}
290+
291+
// unmarshalSqlNullInt32 converts a sql.NullInt32 to an int32.
292+
func unmarshalSqlNullInt32(i sql.NullInt32) int32 {
293+
if i.Valid {
294+
return i.Int32
295+
}
296+
297+
return 0
298+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package reservation
2+
3+
import (
4+
"context"
5+
"crypto/rand"
6+
"testing"
7+
8+
"github.com/btcsuite/btcd/chaincfg/chainhash"
9+
"github.com/btcsuite/btcd/wire"
10+
"github.com/lightninglabs/loop/fsm"
11+
"github.com/lightninglabs/loop/loopdb"
12+
"github.com/lightningnetwork/lnd/keychain"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
// TestSqlStore tests the basic functionality of the SQLStore.
17+
func TestSqlStore(t *testing.T) {
18+
ctxb := context.Background()
19+
testDb := loopdb.NewTestDB(t)
20+
defer testDb.Close()
21+
22+
store := NewSQLStore(testDb)
23+
24+
// Create a reservation and store it.
25+
reservation := &Reservation{
26+
ID: getRandomReservationID(),
27+
State: fsm.StateType("init"),
28+
ClientPubkey: defaultPubkey,
29+
ServerPubkey: defaultPubkey,
30+
Value: 100,
31+
Expiry: 100,
32+
KeyLocator: keychain.KeyLocator{
33+
Family: 1,
34+
Index: 1,
35+
},
36+
}
37+
38+
err := store.CreateReservation(ctxb, reservation)
39+
require.NoError(t, err)
40+
41+
// Get the reservation and compare it.
42+
reservation2, err := store.GetReservation(ctxb, reservation.ID)
43+
require.NoError(t, err)
44+
require.Equal(t, reservation, reservation2)
45+
46+
// Update the reservation and compare it.
47+
reservation.State = fsm.StateType("state2")
48+
err = store.UpdateReservation(ctxb, reservation)
49+
require.NoError(t, err)
50+
51+
reservation2, err = store.GetReservation(ctxb, reservation.ID)
52+
require.NoError(t, err)
53+
require.Equal(t, reservation, reservation2)
54+
55+
// Add an outpoint to the reservation and compare it.
56+
reservation.Outpoint = &wire.OutPoint{
57+
Hash: chainhash.Hash{0x01},
58+
Index: 0,
59+
}
60+
reservation.State = Confirmed
61+
62+
err = store.UpdateReservation(ctxb, reservation)
63+
require.NoError(t, err)
64+
65+
reservation2, err = store.GetReservation(ctxb, reservation.ID)
66+
require.NoError(t, err)
67+
require.Equal(t, reservation, reservation2)
68+
69+
// Add a second reservation.
70+
reservation3 := &Reservation{
71+
ID: getRandomReservationID(),
72+
State: fsm.StateType("init"),
73+
ClientPubkey: defaultPubkey,
74+
ServerPubkey: defaultPubkey,
75+
Value: 99,
76+
Expiry: 100,
77+
KeyLocator: keychain.KeyLocator{
78+
Family: 1,
79+
Index: 1,
80+
},
81+
}
82+
83+
err = store.CreateReservation(ctxb, reservation3)
84+
require.NoError(t, err)
85+
86+
reservations, err := store.ListReservations(ctxb)
87+
require.NoError(t, err)
88+
require.Equal(t, 2, len(reservations))
89+
}
90+
91+
// getRandomReservationID generates a random reservation ID.
92+
func getRandomReservationID() ID {
93+
var id ID
94+
rand.Read(id[:]) // nolint: errcheck
95+
return id
96+
}

0 commit comments

Comments
 (0)