Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to buy reservations for instant outs from server #883

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions cmd/loop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ var (

defaultSwapWaitTime = 30 * time.Minute

defaultRpcTimeout = 30 * time.Second

// maxMsgRecvSize is the largest message our client will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
Expand Down
85 changes: 84 additions & 1 deletion cmd/loop/reservations.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@ package main

import (
"context"
"errors"
"fmt"

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

var reservationsCommands = cli.Command{
var (
reservationAmountFlag = cli.Uint64Flag{
Name: "amt",
Usage: "the amount in satoshis for the reservation",
}
reservationExpiryFlag = cli.UintFlag{
Name: "expiry",
Usage: "the relative block height at which the reservation" +
" expires",
}
)

var reservationsCommands = cli.Command{
Name: "reservations",
ShortName: "r",
Usage: "manage reservations",
Expand All @@ -20,6 +33,7 @@ var reservationsCommands = cli.Command{
`,
Subcommands: []cli.Command{
listReservationsCommand,
newReservationCommand,
},
}

Expand All @@ -34,8 +48,77 @@ var (
`,
Action: listReservations,
}

newReservationCommand = cli.Command{
Name: "new",
ShortName: "n",
Usage: "create a new reservation",
Description: `
Create a new reservation with the given value and expiry.
`,
Action: newReservation,
Flags: []cli.Flag{
reservationAmountFlag,
reservationExpiryFlag,
},
}
)

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

ctxt, cancel := context.WithTimeout(
context.Background(), defaultRpcTimeout,
)
defer cancel()

if !ctx.IsSet(reservationAmountFlag.Name) {
return errors.New("amt flag missing")
}

if !ctx.IsSet(reservationExpiryFlag.Name) {
return errors.New("expiry flag missing")
}

quoteReq, err := client.ReservationQuote(
ctxt, &looprpc.ReservationQuoteRequest{
Amt: ctx.Uint64(reservationAmountFlag.Name),
Expiry: uint32(ctx.Uint(reservationExpiryFlag.Name)),
},
)
if err != nil {
return err
}

fmt.Printf(satAmtFmt, "Reservation Cost: ", quoteReq.PrepayAmt)

fmt.Printf("CONTINUE RESERVATION? (y/n): ")

var answer string
fmt.Scanln(&answer)
if answer == "n" {
return nil
}

reservationRes, err := client.ReservationRequest(
ctxt, &looprpc.ReservationRequestRequest{
Amt: ctx.Uint64(reservationAmountFlag.Name),
Expiry: uint32(ctx.Uint(reservationExpiryFlag.Name)),
MaxPrepayAmt: quoteReq.PrepayAmt,
},
)
if err != nil {
return err
}

printRespJSON(reservationRes)
return nil
}

func listReservations(ctx *cli.Context) error {
client, cleanup, err := getClient(ctx)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion fsm/stateparser/stateparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func run() error {

case "reservation":
reservationFSM := &reservation.FSM{}
err = writeMermaidFile(fp, reservationFSM.GetReservationStates())
err = writeMermaidFile(fp, reservationFSM.GetServerInitiatedReservationStates())
if err != nil {
return err
}
Expand Down
182 changes: 175 additions & 7 deletions instantout/reservation/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,194 @@ package reservation

import (
"context"
"errors"
"fmt"
"time"

"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lnrpc"
)

// InitReservationContext contains the request parameters for a reservation.
type InitReservationContext struct {
const (
// Define route independent max routing fees. We have currently no way
// to get a reliable estimate of the routing fees. Best we can do is
// the minimum routing fees, which is not very indicative.
maxRoutingFeeBase = btcutil.Amount(10)

maxRoutingFeeRate = int64(20000)
)

var (
// The allowed delta between what we accept as the expiry height and
// the actual expiry height.
expiryDelta = uint32(3)

// defaultPrepayTimeout is the default timeout for the prepayment.
DefaultPrepayTimeout = time.Minute * 120
)

// ClientRequestedInitContext contains the request parameters for a reservation.
type ClientRequestedInitContext struct {
value btcutil.Amount
relativeExpiry uint32
heightHint uint32
maxPrepaymentAmt btcutil.Amount
}

// InitFromClientRequestAction is the action that is executed when the
// reservation state machine is initialized from a client request. It creates
// the reservation in the database and sends the reservation request to the
// server.
func (f *FSM) InitFromClientRequestAction(ctx context.Context,
eventCtx fsm.EventContext) fsm.EventType {

// Check if the context is of the correct type.
reservationRequest, ok := eventCtx.(*ClientRequestedInitContext)
if !ok {
return f.HandleError(fsm.ErrInvalidContextType)
}

// Create the reservation in the database.
keyRes, err := f.cfg.Wallet.DeriveNextKey(ctx, KeyFamily)
if err != nil {
return f.HandleError(err)
}

// Send the request to the server.
requestResponse, err := f.cfg.ReservationClient.RequestReservation(
ctx, &swapserverrpc.RequestReservationRequest{
Value: uint64(reservationRequest.value),
Expiry: reservationRequest.relativeExpiry,
ClientKey: keyRes.PubKey.SerializeCompressed(),
},
)
if err != nil {
return f.HandleError(err)
}

expectedExpiry := reservationRequest.relativeExpiry +
reservationRequest.heightHint

// Check that the expiry is in the delta.
if requestResponse.Expiry < expectedExpiry-expiryDelta ||
requestResponse.Expiry > expectedExpiry+expiryDelta {

return f.HandleError(
fmt.Errorf("unexpected expiry height: %v, expected %v",
requestResponse.Expiry, expectedExpiry))
}

prepayment, err := f.cfg.LightningClient.DecodePaymentRequest(
ctx, requestResponse.Invoice,
)
if err != nil {
return f.HandleError(err)
}

if prepayment.Value.ToSatoshis() > reservationRequest.maxPrepaymentAmt {
return f.HandleError(
errors.New("prepayment amount too high"))
}

serverKey, err := btcec.ParsePubKey(requestResponse.ServerKey)
if err != nil {
return f.HandleError(err)
}

var Id ID
copy(Id[:], requestResponse.ReservationId)

reservation, err := NewReservation(
Id, serverKey, keyRes.PubKey, reservationRequest.value,
requestResponse.Expiry, reservationRequest.heightHint,
keyRes.KeyLocator, ProtocolVersionClientInitiated,
)
if err != nil {
return f.HandleError(err)
}
reservation.PrepayInvoice = requestResponse.Invoice
f.reservation = reservation

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

return OnClientInitialized
}

// SendPrepayment is the action that is executed when the reservation
// is initialized from a client request. It dispatches the prepayment to the
// server and wait for it to be settled, signaling confirmation of the
// reservation.
func (f *FSM) SendPrepayment(ctx context.Context,
_ fsm.EventContext) fsm.EventType {

prepayment, err := f.cfg.LightningClient.DecodePaymentRequest(
ctx, f.reservation.PrepayInvoice,
)
if err != nil {
return f.HandleError(err)
}

payReq := lndclient.SendPaymentRequest{
Invoice: f.reservation.PrepayInvoice,
Timeout: DefaultPrepayTimeout,
MaxFee: getMaxRoutingFee(prepayment.Value.ToSatoshis()),
}
// Send the prepayment to the server.
payChan, errChan, err := f.cfg.RouterClient.SendPayment(
ctx, payReq,
)
if err != nil {
return f.HandleError(err)
}

for {
select {
case <-ctx.Done():
return fsm.NoOp

case err := <-errChan:
return f.HandleError(err)

case prepayResp := <-payChan:
if prepayResp.State == lnrpc.Payment_FAILED {
return f.HandleError(
fmt.Errorf("prepayment failed: %v",
prepayResp.FailureReason))
}
if prepayResp.State == lnrpc.Payment_SUCCEEDED {
return OnBroadcast
}
}
}
}

// ServerRequestedInitContext contains the request parameters for a reservation.
type ServerRequestedInitContext 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 (f *FSM) InitAction(ctx context.Context,
// InitFromServerRequestAction is the action that is executed when the
// reservation state machine is initialized from a server request. It creates
// the reservation in the database and dispatches the payment to the server.
func (f *FSM) InitFromServerRequestAction(ctx context.Context,
eventCtx fsm.EventContext) fsm.EventType {

// Check if the context is of the correct type.
reservationRequest, ok := eventCtx.(*InitReservationContext)
reservationRequest, ok := eventCtx.(*ServerRequestedInitContext)
if !ok {
return f.HandleError(fsm.ErrInvalidContextType)
}
Expand Down Expand Up @@ -58,6 +221,7 @@ func (f *FSM) InitAction(ctx context.Context,
reservationRequest.expiry,
reservationRequest.heightHint,
keyRes.KeyLocator,
ProtocolVersionServerInitiated,
)
if err != nil {
return f.HandleError(err)
Expand Down Expand Up @@ -239,3 +403,7 @@ func (f *FSM) handleAsyncError(ctx context.Context, err error) {
f.Errorf("Error sending event: %v", err2)
}
}

func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate)
}
26 changes: 23 additions & 3 deletions instantout/reservation/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ var (
defaultExpiry = uint32(100)
)

func newValidInitReservationContext() *InitReservationContext {
return &InitReservationContext{
func newValidInitReservationContext() *ServerRequestedInitContext {
return &ServerRequestedInitContext{
reservationID: ID{0x01},
serverPubkey: defaultPubkey,
value: defaultValue,
Expand Down Expand Up @@ -80,6 +80,26 @@ func (m *mockReservationClient) FetchL402(ctx context.Context,
args.Error(1)
}

func (m *mockReservationClient) QuoteReservation(ctx context.Context,
in *swapserverrpc.QuoteReservationRequest, opts ...grpc.CallOption) (
*swapserverrpc.QuoteReservationResponse, error) {

args := m.Called(ctx, in, opts)

return args.Get(0).(*swapserverrpc.QuoteReservationResponse),
args.Error(1)
}

func (m *mockReservationClient) RequestReservation(ctx context.Context,
in *swapserverrpc.RequestReservationRequest, opts ...grpc.CallOption) (
*swapserverrpc.RequestReservationResponse, error) {

args := m.Called(ctx, in, opts)

return args.Get(0).(*swapserverrpc.RequestReservationResponse),
args.Error(1)
}

type mockStore struct {
mock.Mock

Expand Down Expand Up @@ -155,7 +175,7 @@ func TestInitReservationAction(t *testing.T) {
StateMachine: &fsm.StateMachine{},
}

event := reservationFSM.InitAction(ctxb, tc.eventCtx)
event := reservationFSM.InitFromServerRequestAction(ctxb, tc.eventCtx)
require.Equal(t, tc.expectedEvent, event)
}
}
Expand Down
Loading
Loading