Skip to content

Commit e9d374a

Browse files
authored
Merge pull request #632 from sputn1ck/instantloopout_2
[2/?] Instant loop out: Add reservations
2 parents e7e0fe5 + 9b178dd commit e9d374a

37 files changed

+3716
-206
lines changed

client.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/lightninglabs/loop/sweep"
2020
"github.com/lightningnetwork/lnd/lntypes"
2121
"github.com/lightningnetwork/lnd/routing/route"
22+
"google.golang.org/grpc"
2223
"google.golang.org/grpc/status"
2324
)
2425

@@ -148,6 +149,7 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
148149
LndServices: cfg.Lnd,
149150
Server: swapServerClient,
150151
Store: loopDB,
152+
Conn: swapServerClient.conn,
151153
LsatStore: lsatStore,
152154
CreateExpiryTimer: func(d time.Duration) <-chan time.Time {
153155
return time.NewTimer(d).C
@@ -200,6 +202,11 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
200202
return client, cleanup, nil
201203
}
202204

205+
// GetConn returns the gRPC connection to the server.
206+
func (s *Client) GetConn() *grpc.ClientConn {
207+
return s.clientConfig.Conn
208+
}
209+
203210
// FetchSwaps returns all loop in and out swaps currently in the database.
204211
func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
205212
loopOutSwaps, err := s.Store.FetchLoopOutSwaps(ctx)

cmd/loop/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ func main() {
147147
monitorCommand, quoteCommand, listAuthCommand,
148148
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
149149
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
150-
getInfoCommand, abandonSwapCommand,
150+
getInfoCommand, abandonSwapCommand, reservationsCommands,
151151
}
152152

153153
err := app.Run(os.Args)

cmd/loop/reservations.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"context"
5+
6+
"github.com/lightninglabs/loop/looprpc"
7+
"github.com/urfave/cli"
8+
)
9+
10+
var reservationsCommands = cli.Command{
11+
12+
Name: "reservations",
13+
ShortName: "r",
14+
Usage: "manage reservations",
15+
Description: `
16+
With loopd running, you can use this command to manage your
17+
reservations. Reservations are 2-of-2 multisig utxos that
18+
the loop server can open to clients. The reservations are used
19+
to enable instant swaps.
20+
`,
21+
Subcommands: []cli.Command{
22+
listReservationsCommand,
23+
},
24+
}
25+
26+
var (
27+
listReservationsCommand = cli.Command{
28+
Name: "list",
29+
ShortName: "l",
30+
Usage: "list all reservations",
31+
ArgsUsage: "",
32+
Description: `
33+
List all reservations.
34+
`,
35+
Action: listReservations,
36+
}
37+
)
38+
39+
func listReservations(ctx *cli.Context) error {
40+
client, cleanup, err := getClient(ctx)
41+
if err != nil {
42+
return err
43+
}
44+
defer cleanup()
45+
46+
resp, err := client.ListReservations(
47+
context.Background(), &looprpc.ListReservationsRequest{},
48+
)
49+
if err != nil {
50+
return err
51+
}
52+
53+
printRespJSON(resp)
54+
return nil
55+
}

config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ import (
66
"github.com/lightninglabs/aperture/lsat"
77
"github.com/lightninglabs/lndclient"
88
"github.com/lightninglabs/loop/loopdb"
9+
"google.golang.org/grpc"
910
)
1011

1112
// clientConfig contains config items for the swap client.
1213
type clientConfig struct {
1314
LndServices *lndclient.LndServices
1415
Server swapServerClient
16+
Conn *grpc.ClientConn
1517
Store loopdb.SwapStore
1618
LsatStore lsat.Store
1719
CreateExpiryTimer func(expiry time.Duration) <-chan time.Time

fsm/fsm.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ func (s *StateMachine) SendEvent(event EventType, eventCtx EventContext) error {
206206
// current state.
207207
state, err := s.getNextState(event)
208208
if err != nil {
209+
log.Errorf("unable to get next state: %v from event: "+
210+
"%v, current state: %v", err, event, s.current)
209211
return ErrEventRejected
210212
}
211213

fsm/stateparser/stateparser.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"sort"
1111

1212
"github.com/lightninglabs/loop/fsm"
13+
"github.com/lightninglabs/loop/instantout/reservation"
1314
)
1415

1516
func main() {
@@ -41,6 +42,13 @@ func run() error {
4142
return err
4243
}
4344

45+
case "reservation":
46+
reservationFSM := &reservation.FSM{}
47+
err = writeMermaidFile(fp, reservationFSM.GetReservationStates())
48+
if err != nil {
49+
return err
50+
}
51+
4452
default:
4553
fmt.Println("Missing or wrong argument: fsm must be one of:")
4654
fmt.Println("\treservations")

instantout/reservation/actions.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package reservation
2+
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcec/v2"
7+
"github.com/btcsuite/btcd/btcutil"
8+
"github.com/lightninglabs/loop/fsm"
9+
looprpc "github.com/lightninglabs/loop/swapserverrpc"
10+
)
11+
12+
// InitReservationContext contains the request parameters for a reservation.
13+
type InitReservationContext struct {
14+
reservationID ID
15+
serverPubkey *btcec.PublicKey
16+
value btcutil.Amount
17+
expiry uint32
18+
heightHint uint32
19+
}
20+
21+
// InitAction is the action that is executed when the reservation state machine
22+
// is initialized. It creates the reservation in the database and dispatches the
23+
// payment to the server.
24+
func (r *FSM) InitAction(eventCtx fsm.EventContext) fsm.EventType {
25+
// Check if the context is of the correct type.
26+
reservationRequest, ok := eventCtx.(*InitReservationContext)
27+
if !ok {
28+
return r.HandleError(fsm.ErrInvalidContextType)
29+
}
30+
31+
keyRes, err := r.cfg.Wallet.DeriveNextKey(
32+
r.ctx, KeyFamily,
33+
)
34+
if err != nil {
35+
return r.HandleError(err)
36+
}
37+
38+
// Send the client reservation details to the server.
39+
log.Debugf("Dispatching reservation to server: %x",
40+
reservationRequest.reservationID)
41+
42+
request := &looprpc.ServerOpenReservationRequest{
43+
ReservationId: reservationRequest.reservationID[:],
44+
ClientKey: keyRes.PubKey.SerializeCompressed(),
45+
}
46+
47+
_, err = r.cfg.ReservationClient.OpenReservation(r.ctx, request)
48+
if err != nil {
49+
return r.HandleError(err)
50+
}
51+
52+
reservation, err := NewReservation(
53+
reservationRequest.reservationID,
54+
reservationRequest.serverPubkey,
55+
keyRes.PubKey,
56+
reservationRequest.value,
57+
reservationRequest.expiry,
58+
reservationRequest.heightHint,
59+
keyRes.KeyLocator,
60+
)
61+
if err != nil {
62+
return r.HandleError(err)
63+
}
64+
65+
r.reservation = reservation
66+
67+
// Create the reservation in the database.
68+
err = r.cfg.Store.CreateReservation(r.ctx, reservation)
69+
if err != nil {
70+
return r.HandleError(err)
71+
}
72+
73+
return OnBroadcast
74+
}
75+
76+
// SubscribeToConfirmationAction is the action that is executed when the
77+
// reservation is waiting for confirmation. It subscribes to the confirmation
78+
// of the reservation transaction.
79+
func (r *FSM) SubscribeToConfirmationAction(_ fsm.EventContext) fsm.EventType {
80+
pkscript, err := r.reservation.GetPkScript()
81+
if err != nil {
82+
return r.HandleError(err)
83+
}
84+
85+
callCtx, cancel := context.WithCancel(r.ctx)
86+
defer cancel()
87+
88+
// Subscribe to the confirmation of the reservation transaction.
89+
log.Debugf("Subscribing to conf for reservation: %x pkscript: %x, "+
90+
"initiation height: %v", r.reservation.ID, pkscript,
91+
r.reservation.InitiationHeight)
92+
93+
confChan, errConfChan, err := r.cfg.ChainNotifier.RegisterConfirmationsNtfn(
94+
callCtx, nil, pkscript, DefaultConfTarget,
95+
r.reservation.InitiationHeight,
96+
)
97+
if err != nil {
98+
r.Errorf("unable to subscribe to conf notification: %v", err)
99+
return r.HandleError(err)
100+
}
101+
102+
blockChan, errBlockChan, err := r.cfg.ChainNotifier.RegisterBlockEpochNtfn(
103+
callCtx,
104+
)
105+
if err != nil {
106+
r.Errorf("unable to subscribe to block notifications: %v", err)
107+
return r.HandleError(err)
108+
}
109+
110+
// We'll now wait for the confirmation of the reservation transaction.
111+
for {
112+
select {
113+
case err := <-errConfChan:
114+
r.Errorf("conf subscription error: %v", err)
115+
return r.HandleError(err)
116+
117+
case err := <-errBlockChan:
118+
r.Errorf("block subscription error: %v", err)
119+
return r.HandleError(err)
120+
121+
case confInfo := <-confChan:
122+
r.Debugf("reservation confirmed: %v", confInfo)
123+
outpoint, err := r.reservation.findReservationOutput(
124+
confInfo.Tx,
125+
)
126+
if err != nil {
127+
return r.HandleError(err)
128+
}
129+
130+
r.reservation.ConfirmationHeight = confInfo.BlockHeight
131+
r.reservation.Outpoint = outpoint
132+
133+
return OnConfirmed
134+
135+
case block := <-blockChan:
136+
r.Debugf("block received: %v expiry: %v", block,
137+
r.reservation.Expiry)
138+
139+
if uint32(block) >= r.reservation.Expiry {
140+
return OnTimedOut
141+
}
142+
143+
case <-r.ctx.Done():
144+
return fsm.NoOp
145+
}
146+
}
147+
}
148+
149+
// ReservationConfirmedAction waits for the reservation to be either expired or
150+
// waits for other actions to happen.
151+
func (r *FSM) ReservationConfirmedAction(_ fsm.EventContext) fsm.EventType {
152+
blockHeightChan, errEpochChan, err := r.cfg.ChainNotifier.
153+
RegisterBlockEpochNtfn(r.ctx)
154+
if err != nil {
155+
return r.HandleError(err)
156+
}
157+
158+
for {
159+
select {
160+
case err := <-errEpochChan:
161+
return r.HandleError(err)
162+
163+
case blockHeight := <-blockHeightChan:
164+
expired := blockHeight >= int32(r.reservation.Expiry)
165+
if expired {
166+
r.Debugf("Reservation %v expired",
167+
r.reservation.ID)
168+
169+
return OnTimedOut
170+
}
171+
172+
case <-r.ctx.Done():
173+
return fsm.NoOp
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)