Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/lightninglabs/loop/sweep"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)

Expand Down Expand Up @@ -148,6 +149,7 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
LndServices: cfg.Lnd,
Server: swapServerClient,
Store: loopDB,
Conn: swapServerClient.conn,
LsatStore: lsatStore,
CreateExpiryTimer: func(d time.Duration) <-chan time.Time {
return time.NewTimer(d).C
Expand Down Expand Up @@ -200,6 +202,11 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
return client, cleanup, nil
}

// GetConn returns the gRPC connection to the server.
func (s *Client) GetConn() *grpc.ClientConn {
return s.clientConfig.Conn
}

// FetchSwaps returns all loop in and out swaps currently in the database.
func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
loopOutSwaps, err := s.Store.FetchLoopOutSwaps(ctx)
Expand Down
2 changes: 1 addition & 1 deletion cmd/loop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func main() {
monitorCommand, quoteCommand, listAuthCommand,
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
getInfoCommand, abandonSwapCommand,
getInfoCommand, abandonSwapCommand, reservationsCommands,
}

err := app.Run(os.Args)
Expand Down
55 changes: 55 additions & 0 deletions cmd/loop/reservations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"context"

"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
)

var reservationsCommands = cli.Command{

Name: "reservations",
ShortName: "r",
Usage: "manage reservations",
Description: `
With loopd running, you can use this command to manage your
reservations. Reservations are 2-of-2 multisig utxos that
the loop server can open to clients. The reservations are used
to enable instant swaps.
`,
Subcommands: []cli.Command{
listReservationsCommand,
},
}

var (
listReservationsCommand = cli.Command{
Name: "list",
ShortName: "l",
Usage: "list all reservations",
ArgsUsage: "",
Description: `
List all reservations.
`,
Action: listReservations,
}
)

func listReservations(ctx *cli.Context) error {
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()

resp, err := client.ListReservations(
context.Background(), &looprpc.ListReservationsRequest{},
)
if err != nil {
return err
}

printRespJSON(resp)
return nil
}
2 changes: 2 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import (
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"google.golang.org/grpc"
)

// clientConfig contains config items for the swap client.
type clientConfig struct {
LndServices *lndclient.LndServices
Server swapServerClient
Conn *grpc.ClientConn
Store loopdb.SwapStore
LsatStore lsat.Store
CreateExpiryTimer func(expiry time.Duration) <-chan time.Time
Expand Down
2 changes: 2 additions & 0 deletions fsm/fsm.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ func (s *StateMachine) SendEvent(event EventType, eventCtx EventContext) error {
// current state.
state, err := s.getNextState(event)
if err != nil {
log.Errorf("unable to get next state: %v from event: "+
"%v, current state: %v", err, event, s.current)
return ErrEventRejected
}

Expand Down
8 changes: 8 additions & 0 deletions fsm/stateparser/stateparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sort"

"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout/reservation"
)

func main() {
Expand Down Expand Up @@ -41,6 +42,13 @@ func run() error {
return err
}

case "reservation":
reservationFSM := &reservation.FSM{}
err = writeMermaidFile(fp, reservationFSM.GetReservationStates())
if err != nil {
return err
}

default:
fmt.Println("Missing or wrong argument: fsm must be one of:")
fmt.Println("\treservations")
Expand Down
176 changes: 176 additions & 0 deletions instantout/reservation/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package reservation

import (
"context"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/loop/fsm"
looprpc "github.com/lightninglabs/loop/swapserverrpc"
)

// InitReservationContext contains the request parameters for a reservation.
type InitReservationContext struct {
reservationID ID
serverPubkey *btcec.PublicKey
value btcutil.Amount
expiry uint32
heightHint uint32
}

// InitAction is the action that is executed when the reservation state machine
// is initialized. It creates the reservation in the database and dispatches the
// payment to the server.
func (r *FSM) InitAction(eventCtx fsm.EventContext) fsm.EventType {
// Check if the context is of the correct type.
reservationRequest, ok := eventCtx.(*InitReservationContext)
if !ok {
return r.HandleError(fsm.ErrInvalidContextType)
}

keyRes, err := r.cfg.Wallet.DeriveNextKey(
r.ctx, KeyFamily,
)
if err != nil {
return r.HandleError(err)
}

// Send the client reservation details to the server.
log.Debugf("Dispatching reservation to server: %x",
reservationRequest.reservationID)

request := &looprpc.ServerOpenReservationRequest{
ReservationId: reservationRequest.reservationID[:],
ClientKey: keyRes.PubKey.SerializeCompressed(),
}

_, err = r.cfg.ReservationClient.OpenReservation(r.ctx, request)
if err != nil {
return r.HandleError(err)
}

reservation, err := NewReservation(
reservationRequest.reservationID,
reservationRequest.serverPubkey,
keyRes.PubKey,
reservationRequest.value,
reservationRequest.expiry,
reservationRequest.heightHint,
keyRes.KeyLocator,
)
if err != nil {
return r.HandleError(err)
}

r.reservation = reservation

// Create the reservation in the database.
err = r.cfg.Store.CreateReservation(r.ctx, reservation)
if err != nil {
return r.HandleError(err)
}

return OnBroadcast
}

// SubscribeToConfirmationAction is the action that is executed when the
// reservation is waiting for confirmation. It subscribes to the confirmation
// of the reservation transaction.
func (r *FSM) SubscribeToConfirmationAction(_ fsm.EventContext) fsm.EventType {
pkscript, err := r.reservation.GetPkScript()
if err != nil {
return r.HandleError(err)
}

callCtx, cancel := context.WithCancel(r.ctx)
defer cancel()

// Subscribe to the confirmation of the reservation transaction.
log.Debugf("Subscribing to conf for reservation: %x pkscript: %x, "+
"initiation height: %v", r.reservation.ID, pkscript,
r.reservation.InitiationHeight)

confChan, errConfChan, err := r.cfg.ChainNotifier.RegisterConfirmationsNtfn(
callCtx, nil, pkscript, DefaultConfTarget,
r.reservation.InitiationHeight,
)
if err != nil {
r.Errorf("unable to subscribe to conf notification: %v", err)
return r.HandleError(err)
}

blockChan, errBlockChan, err := r.cfg.ChainNotifier.RegisterBlockEpochNtfn(
callCtx,
)
if err != nil {
r.Errorf("unable to subscribe to block notifications: %v", err)
return r.HandleError(err)
}

// We'll now wait for the confirmation of the reservation transaction.
for {
select {
case err := <-errConfChan:
r.Errorf("conf subscription error: %v", err)
return r.HandleError(err)

case err := <-errBlockChan:
r.Errorf("block subscription error: %v", err)
return r.HandleError(err)

case confInfo := <-confChan:
r.Debugf("reservation confirmed: %v", confInfo)
outpoint, err := r.reservation.findReservationOutput(
confInfo.Tx,
)
if err != nil {
return r.HandleError(err)
}

r.reservation.ConfirmationHeight = confInfo.BlockHeight
r.reservation.Outpoint = outpoint

return OnConfirmed

case block := <-blockChan:
r.Debugf("block received: %v expiry: %v", block,
r.reservation.Expiry)

if uint32(block) >= r.reservation.Expiry {
return OnTimedOut
}

case <-r.ctx.Done():
return fsm.NoOp
}
}
}

// ReservationConfirmedAction waits for the reservation to be either expired or
// waits for other actions to happen.
func (r *FSM) ReservationConfirmedAction(_ fsm.EventContext) fsm.EventType {
blockHeightChan, errEpochChan, err := r.cfg.ChainNotifier.
RegisterBlockEpochNtfn(r.ctx)
if err != nil {
return r.HandleError(err)
}

for {
select {
case err := <-errEpochChan:
return r.HandleError(err)

case blockHeight := <-blockHeightChan:
expired := blockHeight >= int32(r.reservation.Expiry)
if expired {
r.Debugf("Reservation %v expired",
r.reservation.ID)

return OnTimedOut
}

case <-r.ctx.Done():
return fsm.NoOp
}
}
}
Loading