From 6825e35594bee2ee4322de982e5d6c1122427e1e Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:30:10 +0200 Subject: [PATCH 01/35] looprpc: align instant out permissions with loop out Apply the loop:out permission to Instant Out and reservation RPCs so their authorization requirements match the rest of the Loop Out API. --- looprpc/perms.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/looprpc/perms.go b/looprpc/perms.go index d646f6671..9187920a7 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -177,18 +177,30 @@ var RequiredPermissions = map[string][]bakery.Op{ "/looprpc.SwapClient/ListReservations": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/InstantOut": {{ Entity: "swap", Action: "execute", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/InstantOutQuote": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/ListInstantOuts": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/StopDaemon": {{ Entity: "loop", From a4973caa0812835cd52523f0117dff3dffda9bf7 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:31:08 +0200 Subject: [PATCH 02/35] reservation: keep processing after notification errors Log individual reservation initialization failures and continue consuming later notifications instead of stopping the manager. --- instantout/reservation/manager.go | 3 +- instantout/reservation/manager_test.go | 45 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 600febfe9..b5ff04439 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -80,7 +80,8 @@ func (m *Manager) Run(ctx context.Context, height int32, runCtx, uint32(currentHeight), reservationRes, ) if err != nil { - return err + log.Errorf("Unable to create reservation %x: %v", + reservationRes.ReservationId, err) } case err := <-newBlockErrChan: diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index 79455750c..4b01fcfa4 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -99,6 +99,51 @@ func TestManager(t *testing.T) { require.NoError(t, err) } +// TestManagerContinuesAfterInvalidNotification verifies that a malformed +// server notification doesn't stop the reservation manager from processing +// later notifications. +func TestManagerContinuesAfterInvalidNotification(t *testing.T) { + testContext := newManagerTestContext(t) + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + initChan := make(chan struct{}) + errChan := make(chan error, 1) + go func() { + errChan <- testContext.manager.Run( + ctx, testContext.mockLnd.Height, initChan, + ) + }() + + <-initChan + + // A malformed ID is rejected by newReservation. The manager should log + // the error and continue processing the stream. + testContext.reservationNotificationChan <- &swapserverrpc.ServerReservationNotification{ + ReservationId: []byte{1}, + } + + testContext.reservationNotificationChan <- &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + } + + select { + case <-testContext.mockLnd.RegisterConfChannel: + case err := <-errChan: + require.NoError(t, err) + t.Fatal("reservation manager stopped after malformed notification") + case <-time.After(5 * time.Second): + t.Fatal("valid reservation notification was not processed") + } + + cancel() + require.NoError(t, <-errChan) +} + // ManagerTestContext is a helper struct that contains all the necessary // components to test the reservation manager. type ManagerTestContext struct { From 055a80cf4e3f797a9fac27d96d86861860f4c21b Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:31:55 +0200 Subject: [PATCH 03/35] reservation: isolate asynchronous initialization errors Use a goroutine-local result for event dispatch so observer errors remain independent and initialization outcomes stay deterministic. --- instantout/reservation/manager.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index b5ff04439..0a902c054 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -131,9 +131,11 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, // Send the init event to the state machine. go func() { - err = reservationFSM.SendEvent(ctx, OnServerRequest, initContext) - if err != nil { - log.Errorf("Error sending init event: %v", err) + sendErr := reservationFSM.SendEvent( + ctx, OnServerRequest, initContext, + ) + if sendErr != nil { + log.Errorf("Error sending init event: %v", sendErr) } }() From e11c0bdfaf540c5c94be82bec1d85f7f8e67c121 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:32:52 +0200 Subject: [PATCH 04/35] reservation: reject duplicate reservation entries Check active and persisted reservations before creating a new state machine, preserving the existing reservation when a duplicate arrives. --- instantout/reservation/manager.go | 24 ++++++++++++++++++++- instantout/reservation/manager_test.go | 29 ++++++++++++++++++++++++++ instantout/reservation/store.go | 4 ++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 0a902c054..930dff7bc 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -2,6 +2,7 @@ package reservation import ( "context" + "errors" "fmt" "strings" "sync" @@ -111,13 +112,28 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, return nil, err } + _, err = m.cfg.Store.GetReservation(ctx, reservationID) + switch { + case err == nil: + return nil, ErrReservationAlreadyExists + + case !errors.Is(err, ErrReservationNotFound): + return nil, err + } + // Create the reservation state machine. We need to pass in the runCtx // of the reservation manager so that the state machine will keep on // running even if the grpc conte reservationFSM := NewFSM(m.cfg) - // Add the reservation to the active reservations map. + // Add the reservation to the active reservations map. Check the map while + // holding the lock as concurrent callers may both have completed the store + // lookup above. m.Lock() + if _, ok := m.activeReservations[reservationID]; ok { + m.Unlock() + return nil, ErrReservationAlreadyExists + } m.activeReservations[reservationID] = reservationFSM m.Unlock() @@ -146,6 +162,12 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, fsm.WithWaitForStateOption(time.Second), ) if err != nil { + m.Lock() + if m.activeReservations[reservationID] == reservationFSM { + delete(m.activeReservations, reservationID) + } + m.Unlock() + if reservationFSM.LastActionError != nil { return nil, fmt.Errorf("error waiting for "+ "state: %v, last action error: %v", diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index 4b01fcfa4..0437955ef 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -144,6 +144,35 @@ func TestManagerContinuesAfterInvalidNotification(t *testing.T) { require.NoError(t, <-errChan) } +// TestManagerRejectsDuplicateReservation verifies that a duplicate server +// notification cannot replace the active FSM for an existing reservation. +func TestManagerRejectsDuplicateReservation(t *testing.T) { + testContext := newManagerTestContext(t) + ctx := t.Context() + req := &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + } + + firstFSM, err := testContext.manager.newReservation( + ctx, uint32(testContext.mockLnd.Height), req, + ) + require.NoError(t, err) + + secondFSM, err := testContext.manager.newReservation( + ctx, uint32(testContext.mockLnd.Height), req, + ) + require.ErrorIs(t, err, ErrReservationAlreadyExists) + require.Nil(t, secondFSM) + require.Same( + t, firstFSM, + testContext.manager.activeReservations[defaultReservationId], + ) +} + // ManagerTestContext is a helper struct that contains all the necessary // components to test the reservation manager. type ManagerTestContext struct { diff --git a/instantout/reservation/store.go b/instantout/reservation/store.go index 117d02c69..72613f9ca 100644 --- a/instantout/reservation/store.go +++ b/instantout/reservation/store.go @@ -180,6 +180,10 @@ func (r *SQLStore) GetReservation(ctx context.Context, return nil }) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrReservationNotFound + } + return nil, err } From 94015a5a9adc2d7af36625815aa59b4bcbbfa335 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:34:36 +0200 Subject: [PATCH 05/35] reservation: bound and prune active state machines Limit active reservation state machines, remove terminal entries from memory, and count recovered entries toward the same bound. --- instantout/reservation/interfaces.go | 14 +++++--- instantout/reservation/manager.go | 47 ++++++++++++++++++++++++++ instantout/reservation/manager_test.go | 37 ++++++++++++++++++++ 3 files changed, 93 insertions(+), 5 deletions(-) diff --git a/instantout/reservation/interfaces.go b/instantout/reservation/interfaces.go index 04bf830d3..23658d5a4 100644 --- a/instantout/reservation/interfaces.go +++ b/instantout/reservation/interfaces.go @@ -8,14 +8,18 @@ import ( ) var ( - ErrReservationAlreadyExists = fmt.Errorf("reservation already exists") - ErrReservationNotFound = fmt.Errorf("reservation not found") + ErrReservationAlreadyExists = fmt.Errorf("reservation already exists") + ErrReservationNotFound = fmt.Errorf("reservation not found") + ErrTooManyActiveReservations = fmt.Errorf( + "too many active reservations", + ) ) const ( - KeyFamily = int32(42068) - DefaultConfTarget = int32(3) - IdLength = 32 + KeyFamily = int32(42068) + DefaultConfTarget = int32(3) + IdLength = 32 + maxActiveReservations = 1000 ) // Store is the interface that stores the reservations. diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 930dff7bc..2f4c80c4b 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -26,6 +26,28 @@ type Manager struct { activeReservations map[ID]*FSM } +// finalStateObserver removes a reservation FSM from the active set once it +// reaches a terminal state. +type finalStateObserver struct { + manager *Manager + id ID + fsm *FSM +} + +// Notify implements the fsm.Observer interface. +func (o *finalStateObserver) Notify(notification fsm.Notification) { + if !isFinalState(notification.NextState) { + return + } + + o.manager.Lock() + defer o.manager.Unlock() + + if o.manager.activeReservations[o.id] == o.fsm { + delete(o.manager.activeReservations, o.id) + } +} + // NewManager creates a new reservation manager. func NewManager(cfg *Config) *Manager { return &Manager{ @@ -134,9 +156,19 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, m.Unlock() return nil, ErrReservationAlreadyExists } + if len(m.activeReservations) >= maxActiveReservations { + m.Unlock() + return nil, ErrTooManyActiveReservations + } m.activeReservations[reservationID] = reservationFSM m.Unlock() + reservationFSM.RegisterObserver(&finalStateObserver{ + manager: m, + id: reservationID, + fsm: reservationFSM, + }) + initContext := &InitReservationContext{ reservationID: reservationID, serverPubkey: serverKey, @@ -187,6 +219,16 @@ func (m *Manager) RecoverReservations(ctx context.Context) error { return err } + activeCount := 0 + for _, reservation := range reservations { + if !isFinalState(reservation.State) { + activeCount++ + } + } + if activeCount > maxActiveReservations { + return ErrTooManyActiveReservations + } + for _, reservation := range reservations { if isFinalState(reservation.State) { continue @@ -199,6 +241,11 @@ func (m *Manager) RecoverReservations(ctx context.Context) error { reservationFSM := NewFSMFromReservation(m.cfg, reservation) m.activeReservations[reservation.ID] = reservationFSM + reservationFSM.RegisterObserver(&finalStateObserver{ + manager: m, + id: reservation.ID, + fsm: reservationFSM, + }) // As SendEvent can block, we'll start a goroutine to process // the event. diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index 0437955ef..bf1d1a461 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -97,6 +97,11 @@ func TestManager(t *testing.T) { // We'll now expect the reservation to be expired. err = reservationFSM.DefaultObserver.WaitForState(ctxb, 5*time.Second, Spent) require.NoError(t, err) + + testContext.manager.Lock() + _, ok := testContext.manager.activeReservations[defaultReservationId] + testContext.manager.Unlock() + require.False(t, ok) } // TestManagerContinuesAfterInvalidNotification verifies that a malformed @@ -173,6 +178,38 @@ func TestManagerRejectsDuplicateReservation(t *testing.T) { ) } +// TestManagerLimitsActiveReservations verifies that server notifications +// cannot grow the active FSM set without bound. +func TestManagerLimitsActiveReservations(t *testing.T) { + testContext := newManagerTestContext(t) + + for i := range maxActiveReservations { + var id ID + id[0] = byte(i) + id[1] = byte(i >> 8) + testContext.manager.activeReservations[id] = NewFSM( + testContext.manager.cfg, + ) + } + + reservationFSM, err := testContext.manager.newReservation( + t.Context(), uint32(testContext.mockLnd.Height), + &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + }, + ) + require.ErrorIs(t, err, ErrTooManyActiveReservations) + require.Nil(t, reservationFSM) + require.Len( + t, testContext.manager.activeReservations, + maxActiveReservations, + ) +} + // ManagerTestContext is a helper struct that contains all the necessary // components to test the reservation manager. type ManagerTestContext struct { From a491bcc505335cb64136634a8586fc5664479180 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:35:33 +0200 Subject: [PATCH 06/35] reservation: validate confirmed output amounts Compare each confirmed transaction output with the expected reservation amount before advancing the state machine. --- instantout/reservation/actions_test.go | 10 +++++++++- instantout/reservation/manager_test.go | 1 + instantout/reservation/reservation.go | 11 +++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/instantout/reservation/actions_test.go b/instantout/reservation/actions_test.go index 40e6509b1..643b8ce38 100644 --- a/instantout/reservation/actions_test.go +++ b/instantout/reservation/actions_test.go @@ -203,6 +203,7 @@ func TestSubscribeToConfirmationAction(t *testing.T) { blockHeight int32 blockErr error sendTxConf bool + outputValue btcutil.Amount confErr error expectedEvent fsm.EventType }{ @@ -210,8 +211,15 @@ func TestSubscribeToConfirmationAction(t *testing.T) { name: "success", blockHeight: 0, sendTxConf: true, + outputValue: defaultValue, expectedEvent: OnConfirmed, }, + { + name: "reservation value mismatch", + sendTxConf: true, + outputValue: defaultValue - 1, + expectedEvent: fsm.OnError, + }, { name: "expired", blockHeight: 100, @@ -273,7 +281,7 @@ func TestSubscribeToConfirmationAction(t *testing.T) { TxIn: []*wire.TxIn{}, TxOut: []*wire.TxOut{ { - Value: int64(defaultValue), + Value: int64(tc.outputValue), PkScript: pkScript, }, }, diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index bf1d1a461..e04929c31 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -57,6 +57,7 @@ func TestManager(t *testing.T) { confTx := &wire.MsgTx{ TxOut: []*wire.TxOut{ { + Value: int64(defaultValue), PkScript: pkScript, }, }, diff --git a/instantout/reservation/reservation.go b/instantout/reservation/reservation.go index 5a167d2e1..8b83ae33c 100644 --- a/instantout/reservation/reservation.go +++ b/instantout/reservation/reservation.go @@ -142,8 +142,14 @@ func (r *Reservation) findReservationOutput(tx *wire.MsgTx) (*wire.OutPoint, return nil, err } + var foundScript bool for i, txOut := range tx.TxOut { if bytes.Equal(txOut.PkScript, pkScript) { + foundScript = true + if txOut.Value != int64(r.Value) { + continue + } + return &wire.OutPoint{ Hash: tx.TxHash(), Index: uint32(i), @@ -151,6 +157,11 @@ func (r *Reservation) findReservationOutput(tx *wire.MsgTx) (*wire.OutPoint, } } + if foundScript { + return nil, fmt.Errorf("reservation output value mismatch: "+ + "expected %d", r.Value) + } + return nil, errors.New("reservation output not found") } From 35a2df080f4dfbdfbd682566b74616b394361352 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:36:27 +0200 Subject: [PATCH 07/35] instantout: validate MuSig2 response dimensions Check nonce, signature, session, and transaction input counts before indexing signing vectors, returning clear errors for incomplete data. --- instantout/instantout.go | 38 ++++++++++++++++++++++++++++ instantout/instantout_test.go | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 instantout/instantout_test.go diff --git a/instantout/instantout.go b/instantout/instantout.go index f8c89eb0c..40cbf12e0 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -263,12 +263,31 @@ func (i *InstantOut) signMusig2Tx(ctx context.Context, if err != nil { return nil, err } + if tx == nil { + return nil, errors.New("transaction is nil") + } + if len(tx.TxIn) != len(inputs) { + return nil, fmt.Errorf("invalid number of transaction inputs: "+ + "expected %d, got %d", len(inputs), len(tx.TxIn)) + } + if len(musig2sessions) != len(inputs) { + return nil, fmt.Errorf("invalid number of MuSig2 sessions: "+ + "expected %d, got %d", len(inputs), len(musig2sessions)) + } + if len(counterPartyNonces) != len(inputs) { + return nil, fmt.Errorf("invalid number of server nonces: "+ + "expected %d, got %d", len(inputs), len(counterPartyNonces)) + } prevOutFetcher := inputs.GetPrevoutFetcher() sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) sigs := make([][]byte, len(inputs)) for idx, reservation := range inputs { + if musig2sessions[idx] == nil { + return nil, fmt.Errorf("MuSig2 session %d is nil", idx) + } + if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint, reservation.Outpoint) { @@ -329,8 +348,27 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, if err != nil { return nil, err } + if tx == nil { + return nil, errors.New("transaction is nil") + } + if len(tx.TxIn) != len(inputs) { + return nil, fmt.Errorf("invalid number of transaction inputs: "+ + "expected %d, got %d", len(inputs), len(tx.TxIn)) + } + if len(musig2Sessions) != len(inputs) { + return nil, fmt.Errorf("invalid number of MuSig2 sessions: "+ + "expected %d, got %d", len(inputs), len(musig2Sessions)) + } + if len(serverSigs) != len(inputs) { + return nil, fmt.Errorf("invalid number of server signatures: "+ + "expected %d, got %d", len(inputs), len(serverSigs)) + } for idx := range inputs { + if musig2Sessions[idx] == nil { + return nil, fmt.Errorf("MuSig2 session %d is nil", idx) + } + haveAllSigs, finalSig, err := signer.MuSig2CombineSig( ctx, musig2Sessions[idx].SessionID, [][]byte{serverSigs[idx]}, diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go new file mode 100644 index 000000000..6b0e7cb56 --- /dev/null +++ b/instantout/instantout_test.go @@ -0,0 +1,47 @@ +package instantout + +import ( + "context" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/instantout/reservation" + "github.com/lightningnetwork/lnd/input" + "github.com/stretchr/testify/require" +) + +// TestMuSig2VectorLengthValidation verifies that malformed server-controlled +// vectors are rejected before they can be indexed. +func TestMuSig2VectorLengthValidation(t *testing.T) { + _, pubKey := btcec.PrivKeyFromBytes([]byte{1}) + instantOut := &InstantOut{ + Reservations: []*reservation.Reservation{ + { + ClientPubkey: pubKey, + ServerPubkey: pubKey, + Value: btcutil.Amount(100_000), + Expiry: 200, + Outpoint: &wire.OutPoint{}, + }, + }, + } + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{}) + sessions := []*input.MuSig2SessionInfo{{}} + + require.NotPanics(t, func() { + _, err := instantOut.signMusig2Tx( + context.Background(), nil, tx, sessions, nil, + ) + require.ErrorContains(t, err, "server nonces") + }) + + require.NotPanics(t, func() { + _, err := instantOut.finalizeMusig2Transaction( + context.Background(), nil, sessions, tx, nil, + ) + require.ErrorContains(t, err, "server signatures") + }) +} From e0412ac7752ed6e16f9b8ad923658fda1661973c Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:36:57 +0200 Subject: [PATCH 08/35] instantout: verify finalized MuSig2 witnesses Run script validation for every combined signature before accepting a finalized transaction, surfacing invalid witnesses immediately. --- instantout/instantout.go | 17 ++++++++++++++++ instantout/instantout_test.go | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/instantout/instantout.go b/instantout/instantout.go index 40cbf12e0..16f91d45a 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -364,6 +364,9 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, "expected %d, got %d", len(inputs), len(serverSigs)) } + prevOutFetcher := inputs.GetPrevoutFetcher() + sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) + for idx := range inputs { if musig2Sessions[idx] == nil { return nil, fmt.Errorf("MuSig2 session %d is nil", idx) @@ -382,6 +385,20 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, } tx.TxIn[idx].Witness = wire.TxWitness{finalSig} + + vm, err := txscript.NewEngine( + inputs[idx].PkScript, tx, idx, + txscript.StandardVerifyFlags, nil, sigHashes, + int64(inputs[idx].Value), prevOutFetcher, + ) + if err != nil { + return nil, fmt.Errorf("unable to verify final MuSig2 "+ + "signature for input %d: %w", idx, err) + } + if err := vm.Execute(); err != nil { + return nil, fmt.Errorf("invalid final MuSig2 signature "+ + "for input %d: %w", idx, err) + } } return tx, nil diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index 6b0e7cb56..f2ba3241f 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -7,11 +7,22 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightningnetwork/lnd/input" "github.com/stretchr/testify/require" ) +type invalidFinalSigSigner struct { + lndclient.SignerClient +} + +func (s *invalidFinalSigSigner) MuSig2CombineSig(context.Context, [32]byte, + [][]byte) (bool, []byte, error) { + + return true, make([]byte, 64), nil +} + // TestMuSig2VectorLengthValidation verifies that malformed server-controlled // vectors are rejected before they can be indexed. func TestMuSig2VectorLengthValidation(t *testing.T) { @@ -45,3 +56,29 @@ func TestMuSig2VectorLengthValidation(t *testing.T) { require.ErrorContains(t, err, "server signatures") }) } + +// TestFinalizeMuSig2TransactionVerifiesSignature verifies that a combined +// signature is validated locally before the transaction can be used as the +// instant-out safety net. +func TestFinalizeMuSig2TransactionVerifiesSignature(t *testing.T) { + _, pubKey := btcec.PrivKeyFromBytes([]byte{1}) + res := &reservation.Reservation{ + ClientPubkey: pubKey, + ServerPubkey: pubKey, + Value: btcutil.Amount(100_000), + Expiry: 200, + Outpoint: &wire.OutPoint{}, + } + instantOut := &InstantOut{ + Reservations: []*reservation.Reservation{res}, + } + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{PreviousOutPoint: *res.Outpoint}) + tx.AddTxOut(&wire.TxOut{Value: 90_000}) + + _, err := instantOut.finalizeMusig2Transaction( + context.Background(), &invalidFinalSigSigner{}, + []*input.MuSig2SessionInfo{{}}, tx, [][]byte{{1}}, + ) + require.ErrorContains(t, err, "invalid final MuSig2 signature") +} From adc181ea834679c619dbd81652e3009535b52513 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:37:46 +0200 Subject: [PATCH 09/35] instantout: close unfinished MuSig2 sessions Clean up abandoned signing sessions on error paths while leaving completed sessions to lnd. --- instantout/actions.go | 18 ++++++++++++++++ instantout/instantout.go | 39 ++++++++++++++++++++++++++++++++++- instantout/instantout_test.go | 33 ++++++++++++++++++++++++++++- 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/instantout/actions.go b/instantout/actions.go index d1c405fd8..8a1ca3f07 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -293,6 +293,15 @@ func (f *FSM) BuildHTLCAction(ctx context.Context, } f.htlcMusig2Sessions = htlcSessions + defer func() { + err := cleanupMuSig2Sessions( + ctx, f.cfg.Signer, f.htlcMusig2Sessions, + ) + if err != nil { + f.Errorf("unable to clean up HTLC MuSig2 sessions: %v", err) + } + f.htlcMusig2Sessions = nil + }() // Send the server the client nonces. htlcInitRes, err := f.cfg.InstantOutClient.InitHtlcSig( @@ -382,6 +391,15 @@ func (f *FSM) PushPreimageAction(ctx context.Context, } f.sweeplessSweepSessions = coopSessions + defer func() { + err := cleanupMuSig2Sessions( + ctx, f.cfg.Signer, f.sweeplessSweepSessions, + ) + if err != nil { + f.Errorf("unable to clean up sweep MuSig2 sessions: %v", err) + } + f.sweeplessSweepSessions = nil + }() // Get the feerate for the coop sweep. feeRate, err := f.cfg.Wallet.EstimateFeeRate(ctx, normalConfTarget) diff --git a/instantout/instantout.go b/instantout/instantout.go index 16f91d45a..eade26e0b 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" @@ -25,6 +26,8 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) +const muSig2CleanupTimeout = 5 * time.Second + // InstantOut holds the necessary information to execute an instant out swap. type InstantOut struct { // SwapHash is the hash of the swap. @@ -112,7 +115,10 @@ func (i *InstantOut) createMusig2Session(ctx context.Context, for idx, reservation := range i.Reservations { session, err := reservation.Musig2CreateSession(ctx, signer) if err != nil { - return nil, nil, err + cleanupErr := cleanupMuSig2Sessions( + ctx, signer, musig2Sessions[:idx], + ) + return nil, nil, errors.Join(err, cleanupErr) } musig2Sessions[idx] = session @@ -122,6 +128,32 @@ func (i *InstantOut) createMusig2Session(ctx context.Context, return musig2Sessions, clientNonces, nil } +// cleanupMuSig2Sessions removes completed or abandoned MuSig2 sessions from +// lnd. Cleanup uses a bounded context that survives cancellation of the swap +// action that created the sessions. +func cleanupMuSig2Sessions(ctx context.Context, signer lndclient.SignerClient, + sessions []*input.MuSig2SessionInfo) error { + + cleanupCtx, cancel := context.WithTimeout( + context.WithoutCancel(ctx), muSig2CleanupTimeout, + ) + defer cancel() + + var cleanupErr error + for _, session := range sessions { + if session == nil { + continue + } + + err := signer.MuSig2Cleanup(cleanupCtx, session.SessionID) + if err != nil { + cleanupErr = errors.Join(cleanupErr, err) + } + } + + return cleanupErr +} + // getInputReservations returns the input reservations for the instant out. func (i *InstantOut) getInputReservations() (InputReservations, error) { if len(i.Reservations) == 0 { @@ -384,6 +416,11 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, return nil, fmt.Errorf("missing sigs") } + // lnd removes a MuSig2 session automatically once all signatures + // have been combined. Clear the local entry so the caller's deferred + // cleanup only targets sessions abandoned on an error path. + musig2Sessions[idx] = nil + tx.TxIn[idx].Witness = wire.TxWitness{finalSig} vm, err := txscript.NewEngine( diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index f2ba3241f..fd45b7883 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -23,6 +23,19 @@ func (s *invalidFinalSigSigner) MuSig2CombineSig(context.Context, [32]byte, return true, make([]byte, 64), nil } +type cleanupTrackingSigner struct { + lndclient.SignerClient + + cleaned [][32]byte +} + +func (s *cleanupTrackingSigner) MuSig2Cleanup(_ context.Context, + sessionID [32]byte) error { + + s.cleaned = append(s.cleaned, sessionID) + return nil +} + // TestMuSig2VectorLengthValidation verifies that malformed server-controlled // vectors are rejected before they can be indexed. func TestMuSig2VectorLengthValidation(t *testing.T) { @@ -76,9 +89,27 @@ func TestFinalizeMuSig2TransactionVerifiesSignature(t *testing.T) { tx.AddTxIn(&wire.TxIn{PreviousOutPoint: *res.Outpoint}) tx.AddTxOut(&wire.TxOut{Value: 90_000}) + sessions := []*input.MuSig2SessionInfo{{}} _, err := instantOut.finalizeMusig2Transaction( context.Background(), &invalidFinalSigSigner{}, - []*input.MuSig2SessionInfo{{}}, tx, [][]byte{{1}}, + sessions, tx, [][]byte{{1}}, ) require.ErrorContains(t, err, "invalid final MuSig2 signature") + require.Nil(t, sessions[0]) +} + +// TestCleanupMuSig2Sessions verifies that all allocated sessions are released +// while nil entries from partial session creation are skipped. +func TestCleanupMuSig2Sessions(t *testing.T) { + firstID := [32]byte{1} + secondID := [32]byte{2} + signer := &cleanupTrackingSigner{} + + err := cleanupMuSig2Sessions( + t.Context(), signer, []*input.MuSig2SessionInfo{ + {SessionID: firstID}, nil, {SessionID: secondID}, + }, + ) + require.NoError(t, err) + require.Equal(t, [][32]byte{firstID, secondID}, signer.cleaned) } From cfbf74161b94742b84fd18130d2b86e5114d805d Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:38:27 +0200 Subject: [PATCH 10/35] instantout: recheck reservation timing during recovery Compare reservation expiry with the current height during recovery and use the HTLC path when the remaining window is too short. --- instantout/actions.go | 26 ++++++++++++++++++++++++++ instantout/instantout_test.go | 26 ++++++++++++++++++++++++++ instantout/manager.go | 5 ++++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/instantout/actions.go b/instantout/actions.go index 8a1ca3f07..d1ba889f9 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -63,6 +63,12 @@ type InitInstantOutCtx struct { sweepAddress btcutil.Address } +// RecoverInstantOutCtx contains the chain height at which an instant out is +// resumed after restart. +type RecoverInstantOutCtx struct { + currentHeight int32 +} + // InitInstantOutAction is the first action that is executed when the instant // out FSM is started. It will send the instant out request to the server. func (f *FSM) InitInstantOutAction(ctx context.Context, @@ -382,6 +388,26 @@ func (f *FSM) BuildHTLCAction(ctx context.Context, func (f *FSM) PushPreimageAction(ctx context.Context, eventCtx fsm.EventContext) fsm.EventType { + // A recovered swap may have been offline long enough that the server's + // reservation timeout is now close. Fall back to the already finalized + // HTLC instead of revealing the preimage without enough time to publish + // that safety transaction. + if recoverCtx, ok := eventCtx.(*RecoverInstantOutCtx); ok { + minReservationExpiry := int64(recoverCtx.currentHeight) + + int64(htlcExpiryDelta) + for _, res := range f.InstantOut.Reservations { + if int64(res.Expiry) >= minReservationExpiry { + continue + } + + f.LastActionError = fmt.Errorf("reservation %x expires at "+ + "height %d, before recovery safety height %d", + res.ID, res.Expiry, minReservationExpiry) + + return OnErrorPublishHtlc + } + } + // First we'll create the musig2 context. coopSessions, coopClientNonces, err := f.InstantOut.createMusig2Session( ctx, f.cfg.Signer, diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index fd45b7883..f95218bd0 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightningnetwork/lnd/input" "github.com/stretchr/testify/require" @@ -113,3 +114,28 @@ func TestCleanupMuSig2Sessions(t *testing.T) { require.NoError(t, err) require.Equal(t, [][32]byte{firstID, secondID}, signer.cleaned) } + +// TestPushPreimageRejectsExpiringReservation verifies that recovery takes the +// on-chain fallback before revealing the preimage when a reservation is too +// close to its server-controlled timeout. +func TestPushPreimageRejectsExpiringReservation(t *testing.T) { + instantOutFSM := &FSM{ + StateMachine: &fsm.StateMachine{}, + InstantOut: &InstantOut{ + Reservations: []*reservation.Reservation{ + { + ID: reservation.ID{1}, + Expiry: 139, + }, + }, + }, + } + + event := instantOutFSM.PushPreimageAction( + t.Context(), &RecoverInstantOutCtx{currentHeight: 100}, + ) + require.Equal(t, OnErrorPublishHtlc, event) + require.ErrorContains( + t, instantOutFSM.LastActionError, "before recovery safety height", + ) +} diff --git a/instantout/manager.go b/instantout/manager.go index 37ccb681b..9413f5bbd 100644 --- a/instantout/manager.go +++ b/instantout/manager.go @@ -119,8 +119,11 @@ func (m *Manager) recoverInstantOuts(ctx context.Context) error { // As SendEvent can block, we'll start a goroutine to process // the event. + recoverCtx := &RecoverInstantOutCtx{ + currentHeight: m.currentHeight, + } go func() { - err := instantOutFSM.SendEvent(ctx, OnRecover, nil) + err := instantOutFSM.SendEvent(ctx, OnRecover, recoverCtx) if err != nil { log.Errorf("FSM %v Error sending recover "+ "event %v, state: %v", From 63a0bccf3d6c4299014fda3f82abe25706209de4 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:43:16 +0200 Subject: [PATCH 11/35] instantout: enforce the accepted swap fee Carry the accepted quote into each request, persist it, and reject invoices above that limit while retaining millisatoshi precision. --- cmd/loop/instantout.go | 1 + .../instantout/02_loop-instantout.json | 3 +- .../07_loop-instantout-channel.json | 3 +- .../08_loop-instantout-select-index.json | 3 +- instantout/actions.go | 39 +++++++++++- instantout/instantout.go | 3 + instantout/instantout_test.go | 62 +++++++++++++++++++ instantout/manager.go | 8 ++- instantout/store.go | 3 +- loopd/swapclient_server.go | 1 + looprpc/client.pb.go | 16 ++++- looprpc/client.proto | 5 ++ looprpc/client.swagger.json | 5 ++ 13 files changed, 141 insertions(+), 11 deletions(-) diff --git a/cmd/loop/instantout.go b/cmd/loop/instantout.go index 9783f0054..4295222b1 100644 --- a/cmd/loop/instantout.go +++ b/cmd/loop/instantout.go @@ -190,6 +190,7 @@ func instantOut(ctx context.Context, cmd *cli.Command) error { ReservationIds: selectedReservations, OutgoingChanSet: outgoingChanSet, DestAddr: cmd.String("addr"), + MaxSwapFeeSat: quote.ServiceFeeSat, }, ) if err != nil { diff --git a/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json b/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json index 6dae20b15..257f27c7c 100644 --- a/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json +++ b/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json @@ -139,7 +139,8 @@ "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=" ], "outgoing_chan_set": [], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "4800" } } }, diff --git a/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json b/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json index 205ae43e8..07038f8e1 100644 --- a/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json +++ b/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json @@ -148,7 +148,8 @@ "outgoing_chan_set": [ "125344325763072" ], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "3200" } } }, diff --git a/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json b/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json index 9a6d103b3..89f13c318 100644 --- a/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json +++ b/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json @@ -162,7 +162,8 @@ "cSfKVONNmsK9+p4Uc5nc3ZtE+37uOODHeq1vprhh/x4=" ], "outgoing_chan_set": [], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "1600" } } }, diff --git a/instantout/actions.go b/instantout/actions.go index d1ba889f9..8353ee1e8 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" ) const ( @@ -61,6 +62,7 @@ type InitInstantOutCtx struct { outgoingChanSet loopdb.ChannelSet protocolVersion ProtocolVersion sweepAddress btcutil.Address + maxSwapFee btcutil.Amount } // RecoverInstantOutCtx contains the chain height at which an instant out is @@ -84,7 +86,7 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, } var ( - reservationAmt uint64 + reservationAmt btcutil.Amount reservationIds = make([][]byte, 0, len(initCtx.reservations)) reservations = make( []*reservation.Reservation, 0, len(initCtx.reservations), @@ -105,7 +107,7 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, "locked", reservationId)) } - reservationAmt += uint64(res.Value) + reservationAmt += res.Value reservationIds = append(reservationIds, resId[:]) reservations = append(reservations, res) @@ -167,6 +169,11 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, return f.HandleError(fmt.Errorf("invalid swap invoice hash: "+ "expected %x got %x", preimage.Hash(), payReq.Hash)) } + if err := validateInstantOutInvoiceAmount( + payReq.Value, reservationAmt, initCtx.maxSwapFee, + ); err != nil { + return f.HandleError(err) + } serverPubkey, err := btcec.ParsePubKey(instantOutResponse.SenderKey) if err != nil { return f.HandleError(err) @@ -194,7 +201,8 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, CltvExpiry: initCtx.cltvExpiry, clientPubkey: keyRes.PubKey, serverPubkey: serverPubkey, - Value: btcutil.Amount(reservationAmt), + Value: reservationAmt, + MaxSwapFee: initCtx.maxSwapFee, htlcFeeRate: feeRate, swapInvoice: instantOutResponse.SwapInvoice, Reservations: reservations, @@ -212,6 +220,31 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, return OnInit } +// validateInstantOutInvoiceAmount verifies that the server invoice doesn't +// charge more than the client-approved swap fee. Sub-satoshi fees are rounded +// up so the cap cannot be bypassed with millisatoshi precision. +func validateInstantOutInvoiceAmount(invoiceAmount lnwire.MilliSatoshi, + swapAmount, maxSwapFee btcutil.Amount) error { + + if maxSwapFee < 0 { + return fmt.Errorf("maximum swap fee must not be negative") + } + + swapAmountMsat := lnwire.NewMSatFromSatoshis(swapAmount) + if invoiceAmount <= swapAmountMsat { + return nil + } + + swapFeeMsat := invoiceAmount - swapAmountMsat + swapFeeSat := btcutil.Amount((int64(swapFeeMsat)-1)/1000 + 1) + if swapFeeSat > maxSwapFee { + return fmt.Errorf("instant out swap fee %d exceeds maximum %d", + swapFeeSat, maxSwapFee) + } + + return nil +} + // PollPaymentAcceptedAction locks the reservations, sends the payment to the // server and polls the server for the payment status. func (f *FSM) PollPaymentAcceptedAction(ctx context.Context, diff --git a/instantout/instantout.go b/instantout/instantout.go index eade26e0b..c700ee917 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -60,6 +60,9 @@ type InstantOut struct { // Value is the amount that is swapped. Value btcutil.Amount + // MaxSwapFee is the maximum off-chain swap fee accepted by the client. + MaxSwapFee btcutil.Amount + // keyLocator is the key locator that is used for the swap. keyLocator keychain.KeyLocator diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go index f95218bd0..ea2086591 100644 --- a/instantout/instantout_test.go +++ b/instantout/instantout_test.go @@ -11,6 +11,7 @@ import ( "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/instantout/reservation" "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" ) @@ -139,3 +140,64 @@ func TestPushPreimageRejectsExpiringReservation(t *testing.T) { t, instantOutFSM.LastActionError, "before recovery safety height", ) } + +// TestValidateInstantOutInvoiceAmount verifies enforcement of the fee cap at +// millisatoshi precision. +func TestValidateInstantOutInvoiceAmount(t *testing.T) { + const ( + swapAmount = btcutil.Amount(100_000) + maxSwapFee = btcutil.Amount(200) + ) + + tests := []struct { + name string + invoiceAmount lnwire.MilliSatoshi + maxSwapFee btcutil.Amount + expectErr bool + }{ + { + name: "exact fee cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount + maxSwapFee, + ), + maxSwapFee: maxSwapFee, + }, + { + name: "one millisatoshi over fee cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount+maxSwapFee, + ) + 1, + maxSwapFee: maxSwapFee, + expectErr: true, + }, + { + name: "discounted invoice", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount - 1, + ), + maxSwapFee: 0, + }, + { + name: "negative cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount, + ), + maxSwapFee: -1, + expectErr: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validateInstantOutInvoiceAmount( + tc.invoiceAmount, swapAmount, tc.maxSwapFee, + ) + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} diff --git a/instantout/manager.go b/instantout/manager.go index 9413f5bbd..bebd8f824 100644 --- a/instantout/manager.go +++ b/instantout/manager.go @@ -138,7 +138,12 @@ func (m *Manager) recoverInstantOuts(ctx context.Context) error { // NewInstantOut creates a new instantout. func (m *Manager) NewInstantOut(ctx context.Context, - reservations []reservation.ID, sweepAddress string) (*FSM, error) { + reservations []reservation.ID, sweepAddress string, + maxSwapFee btcutil.Amount) (*FSM, error) { + + if maxSwapFee < 0 { + return nil, fmt.Errorf("maximum swap fee must not be negative") + } var ( sweepAddr btcutil.Address @@ -161,6 +166,7 @@ func (m *Manager) NewInstantOut(ctx context.Context, initationHeight: m.currentHeight, protocolVersion: CurrentProtocolVersion(), sweepAddress: sweepAddr, + maxSwapFee: maxSwapFee, } instantOut, err := NewFSM(m.cfg, ProtocolVersionFullReservation) diff --git a/instantout/store.go b/instantout/store.go index 25d7fe704..0e2a5066f 100644 --- a/instantout/store.go +++ b/instantout/store.go @@ -104,7 +104,7 @@ func (s *SQLStore) CreateInstantLoopOut(ctx context.Context, AmountRequested: int64(instantOut.Value), CltvExpiry: instantOut.CltvExpiry, MaxMinerFee: 0, - MaxSwapFee: 0, + MaxSwapFee: int64(instantOut.MaxSwapFee), InitiationHeight: instantOut.initiationHeight, ProtocolVersion: int32(instantOut.protocolVersion), Label: "", @@ -368,6 +368,7 @@ func (s *SQLStore) sqlInstantOutToInstantOut(ctx context.Context, protocolVersion: ProtocolVersion(row.ProtocolVersion), initiationHeight: row.InitiationHeight, Value: btcutil.Amount(row.AmountRequested), + MaxSwapFee: btcutil.Amount(row.MaxSwapFee), keyLocator: keychain.KeyLocator{ Family: keychain.KeyFamily(row.ClientKeyFamily), Index: uint32(row.ClientKeyIndex), diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 28b4f722e..dd5e0f051 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -1765,6 +1765,7 @@ func (s *swapClientServer) InstantOut(ctx context.Context, instantOutFsm, err := s.instantOutManager.NewInstantOut( ctx, reservationIds, req.DestAddr, + btcutil.Amount(req.MaxSwapFeeSat), ) if err != nil { return nil, err diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index ec3246482..53fb113f9 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -4467,7 +4467,9 @@ type InstantOutRequest struct { OutgoingChanSet []uint64 `protobuf:"varint,2,rep,packed,name=outgoing_chan_set,json=outgoingChanSet,proto3" json:"outgoing_chan_set,omitempty"` // An optional address to sweep the onchain funds to. If not set, the funds // will be swept to the wallet's internal address. - DestAddr string `protobuf:"bytes,3,opt,name=dest_addr,json=destAddr,proto3" json:"dest_addr,omitempty"` + DestAddr string `protobuf:"bytes,3,opt,name=dest_addr,json=destAddr,proto3" json:"dest_addr,omitempty"` + // The maximum off-chain swap fee that may be charged for the swap. + MaxSwapFeeSat int64 `protobuf:"varint,4,opt,name=max_swap_fee_sat,json=maxSwapFeeSat,proto3" json:"max_swap_fee_sat,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4523,6 +4525,13 @@ func (x *InstantOutRequest) GetDestAddr() string { return "" } +func (x *InstantOutRequest) GetMaxSwapFeeSat() int64 { + if x != nil { + return x.MaxSwapFeeSat + } + return 0 +} + type InstantOutResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The hash of the swap preimage. @@ -6964,11 +6973,12 @@ const file_client_proto_rawDesc = "" + "\x06amount\x18\x03 \x01(\x04R\x06amount\x12\x13\n" + "\x05tx_id\x18\x04 \x01(\tR\x04txId\x12\x12\n" + "\x04vout\x18\x05 \x01(\rR\x04vout\x12\x16\n" + - "\x06expiry\x18\x06 \x01(\rR\x06expiry\"\x85\x01\n" + + "\x06expiry\x18\x06 \x01(\rR\x06expiry\"\xae\x01\n" + "\x11InstantOutRequest\x12'\n" + "\x0freservation_ids\x18\x01 \x03(\fR\x0ereservationIds\x12*\n" + "\x11outgoing_chan_set\x18\x02 \x03(\x04R\x0foutgoingChanSet\x12\x1b\n" + - "\tdest_addr\x18\x03 \x01(\tR\bdestAddr\"t\n" + + "\tdest_addr\x18\x03 \x01(\tR\bdestAddr\x12'\n" + + "\x10max_swap_fee_sat\x18\x04 \x01(\x03R\rmaxSwapFeeSat\"t\n" + "\x12InstantOutResponse\x12(\n" + "\x10instant_out_hash\x18\x01 \x01(\fR\x0einstantOutHash\x12\x1e\n" + "\vsweep_tx_id\x18\x02 \x01(\tR\tsweepTxId\x12\x14\n" + diff --git a/looprpc/client.proto b/looprpc/client.proto index 844dade7b..d36203008 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -1685,6 +1685,11 @@ message InstantOutRequest { will be swept to the wallet's internal address. */ string dest_addr = 3; + + /* + The maximum off-chain swap fee that may be charged for the swap. + */ + int64 max_swap_fee_sat = 4; } message InstantOutResponse { diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index 1c94febb9..b75794320 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -1917,6 +1917,11 @@ "dest_addr": { "type": "string", "description": "An optional address to sweep the onchain funds to. If not set, the funds\nwill be swept to the wallet's internal address." + }, + "max_swap_fee_sat": { + "type": "string", + "format": "int64", + "description": "The maximum off-chain swap fee that may be charged for the swap." } } }, From 4f07354ff592255be87b3bf6d6981af60bf6e0aa Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 11:43:54 +0200 Subject: [PATCH 12/35] docs: document instant out reliability improvements Record the reservation and Instant Out validation, recovery, fee-limit, and lifecycle updates in the next release notes. --- docs/release-notes/release-notes-next.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md index f0ca7972d..f7f242698 100644 --- a/docs/release-notes/release-notes-next.md +++ b/docs/release-notes/release-notes-next.md @@ -6,6 +6,10 @@ #### Bug Fixes +* Hardened Instant Out and reservation handling against malformed server + responses, invalid signatures and reservation outputs, resource exhaustion, + unsafe recovery, excessive swap fees, and under-scoped macaroon permissions. + * Taproot Asset Loop Out handling now validates RFQ timeouts and asset rates, keeps cached asset-name lookups responsive during slow `tapd` queries, and closes `tapd` connections cleanly during shutdown and startup failures. From 96572f39ba0b0aa33ceb6e851c48404f62a86108 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 3 Feb 2025 14:00:10 +0100 Subject: [PATCH 13/35] reservation: add protocol version --- instantout/reservation/fsm.go | 5 ++--- instantout/reservation/manager.go | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/instantout/reservation/fsm.go b/instantout/reservation/fsm.go index 946d5102d..f61e5097c 100644 --- a/instantout/reservation/fsm.go +++ b/instantout/reservation/fsm.go @@ -60,10 +60,10 @@ type FSM struct { } // NewFSM creates a new reservation FSM. -func NewFSM(cfg *Config) *FSM { +func NewFSM(cfg *Config, protocolVersion ProtocolVersion) *FSM { reservation := &Reservation{ State: fsm.EmptyState, - ProtocolVersion: CurrentProtocolVersion, + ProtocolVersion: protocolVersion, } return NewFSMFromReservation(cfg, reservation) @@ -81,7 +81,6 @@ func NewFSMFromReservation(cfg *Config, reservation *Reservation) *FSM { switch reservation.ProtocolVersion { case ProtocolVersionServerInitiated: states = reservationFsm.GetServerInitiatedReservationStates() - default: states = make(fsm.States) } diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 2f4c80c4b..78251a554 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -146,7 +146,7 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, // Create the reservation state machine. We need to pass in the runCtx // of the reservation manager so that the state machine will keep on // running even if the grpc conte - reservationFSM := NewFSM(m.cfg) + reservationFSM := NewFSM(m.cfg, ProtocolVersionServerInitiated) // Add the reservation to the active reservations map. Check the map while // holding the lock as concurrent callers may both have completed the store From af017413a316141680dcaff9aa755e4a60ba008d Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Fri, 31 Jan 2025 09:30:07 +0100 Subject: [PATCH 14/35] swapserverrpc: add buying reservations --- instantout/reservation/actions_test.go | 20 ++ swapserverrpc/reservation.pb.go | 287 ++++++++++++++++++++++++- swapserverrpc/reservation.proto | 53 +++++ swapserverrpc/reservation_grpc.pb.go | 76 +++++++ 4 files changed, 427 insertions(+), 9 deletions(-) diff --git a/instantout/reservation/actions_test.go b/instantout/reservation/actions_test.go index 643b8ce38..2120e02e4 100644 --- a/instantout/reservation/actions_test.go +++ b/instantout/reservation/actions_test.go @@ -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 diff --git a/swapserverrpc/reservation.pb.go b/swapserverrpc/reservation.pb.go index a1f73b43d..553a3e546 100644 --- a/swapserverrpc/reservation.pb.go +++ b/swapserverrpc/reservation.pb.go @@ -301,6 +301,248 @@ func (*ServerOpenReservationResponse) Descriptor() ([]byte, []int) { return file_reservation_proto_rawDescGZIP(), []int{3} } +// RequestReservationRequest is a request sent from the client to the server to +// request a new reservation UTXO. +type RequestReservationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // value is the value of the reservation in satoshis. + Value uint64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + // expiry is the relative expiry of the reservation. + Expiry uint32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"` + // client_key is the public key of the client. + ClientKey []byte `protobuf:"bytes,3,opt,name=client_key,json=clientKey,proto3" json:"client_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestReservationRequest) Reset() { + *x = RequestReservationRequest{} + mi := &file_reservation_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestReservationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestReservationRequest) ProtoMessage() {} + +func (x *RequestReservationRequest) ProtoReflect() protoreflect.Message { + mi := &file_reservation_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestReservationRequest.ProtoReflect.Descriptor instead. +func (*RequestReservationRequest) Descriptor() ([]byte, []int) { + return file_reservation_proto_rawDescGZIP(), []int{4} +} + +func (x *RequestReservationRequest) GetValue() uint64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *RequestReservationRequest) GetExpiry() uint32 { + if x != nil { + return x.Expiry + } + return 0 +} + +func (x *RequestReservationRequest) GetClientKey() []byte { + if x != nil { + return x.ClientKey + } + return nil +} + +// RequestReservationResponse is a response sent from the server to the client +// to confirm a reservation request. +type RequestReservationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // reservation_id is the id of the reservation. + ReservationId []byte `protobuf:"bytes,1,opt,name=reservation_id,json=reservationId,proto3" json:"reservation_id,omitempty"` + // server_key is the public key of the server. + ServerKey []byte `protobuf:"bytes,2,opt,name=server_key,json=serverKey,proto3" json:"server_key,omitempty"` + // invoice is the invoice for the reservation that the client should pay. + Invoice string `protobuf:"bytes,3,opt,name=invoice,proto3" json:"invoice,omitempty"` + // expiry is the absolute expiry of the reservation. + Expiry uint32 `protobuf:"varint,4,opt,name=expiry,proto3" json:"expiry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestReservationResponse) Reset() { + *x = RequestReservationResponse{} + mi := &file_reservation_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestReservationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestReservationResponse) ProtoMessage() {} + +func (x *RequestReservationResponse) ProtoReflect() protoreflect.Message { + mi := &file_reservation_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestReservationResponse.ProtoReflect.Descriptor instead. +func (*RequestReservationResponse) Descriptor() ([]byte, []int) { + return file_reservation_proto_rawDescGZIP(), []int{5} +} + +func (x *RequestReservationResponse) GetReservationId() []byte { + if x != nil { + return x.ReservationId + } + return nil +} + +func (x *RequestReservationResponse) GetServerKey() []byte { + if x != nil { + return x.ServerKey + } + return nil +} + +func (x *RequestReservationResponse) GetInvoice() string { + if x != nil { + return x.Invoice + } + return "" +} + +func (x *RequestReservationResponse) GetExpiry() uint32 { + if x != nil { + return x.Expiry + } + return 0 +} + +// QuoteReservationRequest is a request sent from the client to the server to +// request a quote for a reservation UTXO. +type QuoteReservationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // value is the value of the reservation in satoshis. + Value uint64 `protobuf:"varint,1,opt,name=value,proto3" json:"value,omitempty"` + // expiry is the relative expiry of the reservation. + Expiry uint32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuoteReservationRequest) Reset() { + *x = QuoteReservationRequest{} + mi := &file_reservation_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuoteReservationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuoteReservationRequest) ProtoMessage() {} + +func (x *QuoteReservationRequest) ProtoReflect() protoreflect.Message { + mi := &file_reservation_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QuoteReservationRequest.ProtoReflect.Descriptor instead. +func (*QuoteReservationRequest) Descriptor() ([]byte, []int) { + return file_reservation_proto_rawDescGZIP(), []int{6} +} + +func (x *QuoteReservationRequest) GetValue() uint64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *QuoteReservationRequest) GetExpiry() uint32 { + if x != nil { + return x.Expiry + } + return 0 +} + +// QuoteReservationResponse is a response sent from the server to the client to +// confirm a reservation quote request. +type QuoteReservationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // prepay_cost is the cost of the prepay. + PrepayCost uint64 `protobuf:"varint,1,opt,name=prepay_cost,json=prepayCost,proto3" json:"prepay_cost,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *QuoteReservationResponse) Reset() { + *x = QuoteReservationResponse{} + mi := &file_reservation_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *QuoteReservationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*QuoteReservationResponse) ProtoMessage() {} + +func (x *QuoteReservationResponse) ProtoReflect() protoreflect.Message { + mi := &file_reservation_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use QuoteReservationResponse.ProtoReflect.Descriptor instead. +func (*QuoteReservationResponse) Descriptor() ([]byte, []int) { + return file_reservation_proto_rawDescGZIP(), []int{7} +} + +func (x *QuoteReservationResponse) GetPrepayCost() uint64 { + if x != nil { + return x.PrepayCost + } + return 0 +} + var File_reservation_proto protoreflect.FileDescriptor const file_reservation_proto_rawDesc = "" + @@ -319,13 +561,32 @@ const file_reservation_proto_rawDesc = "" + "\x0ereservation_id\x18\x01 \x01(\fR\rreservationId\x12\x1d\n" + "\n" + "client_key\x18\x02 \x01(\fR\tclientKey\"\x1f\n" + - "\x1dServerOpenReservationResponse*Q\n" + + "\x1dServerOpenReservationResponse\"h\n" + + "\x19RequestReservationRequest\x12\x14\n" + + "\x05value\x18\x01 \x01(\x04R\x05value\x12\x16\n" + + "\x06expiry\x18\x02 \x01(\rR\x06expiry\x12\x1d\n" + + "\n" + + "client_key\x18\x03 \x01(\fR\tclientKey\"\x94\x01\n" + + "\x1aRequestReservationResponse\x12%\n" + + "\x0ereservation_id\x18\x01 \x01(\fR\rreservationId\x12\x1d\n" + + "\n" + + "server_key\x18\x02 \x01(\fR\tserverKey\x12\x18\n" + + "\ainvoice\x18\x03 \x01(\tR\ainvoice\x12\x16\n" + + "\x06expiry\x18\x04 \x01(\rR\x06expiry\"G\n" + + "\x17QuoteReservationRequest\x12\x14\n" + + "\x05value\x18\x01 \x01(\x04R\x05value\x12\x16\n" + + "\x06expiry\x18\x02 \x01(\rR\x06expiry\";\n" + + "\x18QuoteReservationResponse\x12\x1f\n" + + "\vprepay_cost\x18\x01 \x01(\x04R\n" + + "prepayCost*Q\n" + "\x1aReservationProtocolVersion\x12\x14\n" + "\x10RESERVATION_NONE\x10\x00\x12\x1d\n" + - "\x19RESERVATION_SERVER_NOTIFY\x10\x012\xef\x01\n" + + "\x19RESERVATION_SERVER_NOTIFY\x10\x012\xa7\x03\n" + "\x12ReservationService\x12w\n" + "\x1dReservationNotificationStream\x12'.looprpc.ReservationNotificationRequest\x1a&.looprpc.ServerReservationNotification\"\x03\x88\x02\x010\x01\x12`\n" + - "\x0fOpenReservation\x12%.looprpc.ServerOpenReservationRequest\x1a&.looprpc.ServerOpenReservationResponseB-Z+github.com/lightninglabs/loop/swapserverrpcb\x06proto3" + "\x0fOpenReservation\x12%.looprpc.ServerOpenReservationRequest\x1a&.looprpc.ServerOpenReservationResponse\x12]\n" + + "\x12RequestReservation\x12\".looprpc.RequestReservationRequest\x1a#.looprpc.RequestReservationResponse\x12W\n" + + "\x10QuoteReservation\x12 .looprpc.QuoteReservationRequest\x1a!.looprpc.QuoteReservationResponseB-Z+github.com/lightninglabs/loop/swapserverrpcb\x06proto3" var ( file_reservation_proto_rawDescOnce sync.Once @@ -340,23 +601,31 @@ func file_reservation_proto_rawDescGZIP() []byte { } var file_reservation_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_reservation_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_reservation_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_reservation_proto_goTypes = []any{ (ReservationProtocolVersion)(0), // 0: looprpc.ReservationProtocolVersion (*ReservationNotificationRequest)(nil), // 1: looprpc.ReservationNotificationRequest (*ServerReservationNotification)(nil), // 2: looprpc.ServerReservationNotification (*ServerOpenReservationRequest)(nil), // 3: looprpc.ServerOpenReservationRequest (*ServerOpenReservationResponse)(nil), // 4: looprpc.ServerOpenReservationResponse + (*RequestReservationRequest)(nil), // 5: looprpc.RequestReservationRequest + (*RequestReservationResponse)(nil), // 6: looprpc.RequestReservationResponse + (*QuoteReservationRequest)(nil), // 7: looprpc.QuoteReservationRequest + (*QuoteReservationResponse)(nil), // 8: looprpc.QuoteReservationResponse } var file_reservation_proto_depIdxs = []int32{ 0, // 0: looprpc.ReservationNotificationRequest.protocol_version:type_name -> looprpc.ReservationProtocolVersion 0, // 1: looprpc.ServerReservationNotification.protocol_version:type_name -> looprpc.ReservationProtocolVersion 1, // 2: looprpc.ReservationService.ReservationNotificationStream:input_type -> looprpc.ReservationNotificationRequest 3, // 3: looprpc.ReservationService.OpenReservation:input_type -> looprpc.ServerOpenReservationRequest - 2, // 4: looprpc.ReservationService.ReservationNotificationStream:output_type -> looprpc.ServerReservationNotification - 4, // 5: looprpc.ReservationService.OpenReservation:output_type -> looprpc.ServerOpenReservationResponse - 4, // [4:6] is the sub-list for method output_type - 2, // [2:4] is the sub-list for method input_type + 5, // 4: looprpc.ReservationService.RequestReservation:input_type -> looprpc.RequestReservationRequest + 7, // 5: looprpc.ReservationService.QuoteReservation:input_type -> looprpc.QuoteReservationRequest + 2, // 6: looprpc.ReservationService.ReservationNotificationStream:output_type -> looprpc.ServerReservationNotification + 4, // 7: looprpc.ReservationService.OpenReservation:output_type -> looprpc.ServerOpenReservationResponse + 6, // 8: looprpc.ReservationService.RequestReservation:output_type -> looprpc.RequestReservationResponse + 8, // 9: looprpc.ReservationService.QuoteReservation:output_type -> looprpc.QuoteReservationResponse + 6, // [6:10] is the sub-list for method output_type + 2, // [2:6] is the sub-list for method input_type 2, // [2:2] is the sub-list for extension type_name 2, // [2:2] is the sub-list for extension extendee 0, // [0:2] is the sub-list for field type_name @@ -373,7 +642,7 @@ func file_reservation_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_reservation_proto_rawDesc), len(file_reservation_proto_rawDesc)), NumEnums: 1, - NumMessages: 4, + NumMessages: 8, NumExtensions: 0, NumServices: 1, }, diff --git a/swapserverrpc/reservation.proto b/swapserverrpc/reservation.proto index b582c6e69..0d713eb78 100644 --- a/swapserverrpc/reservation.proto +++ b/swapserverrpc/reservation.proto @@ -19,6 +19,14 @@ service ReservationService { // OpenReservation requests a new reservation UTXO from the server. rpc OpenReservation (ServerOpenReservationRequest) returns (ServerOpenReservationResponse); + + // RequestReservation requests a new reservation UTXO from the server. + rpc RequestReservation (RequestReservationRequest) + returns (RequestReservationResponse); + + // QuoteReservation requests a quote for a reservation UTXO from the server. + rpc QuoteReservation (QuoteReservationRequest) + returns (QuoteReservationResponse); } // ReservationNotificationRequest is an empty request sent from the client to @@ -62,6 +70,51 @@ message ServerOpenReservationRequest { message ServerOpenReservationResponse { } +// RequestReservationRequest is a request sent from the client to the server to +// request a new reservation UTXO. +message RequestReservationRequest { + // value is the value of the reservation in satoshis. + uint64 value = 1; + + // expiry is the relative expiry of the reservation. + uint32 expiry = 2; + + // client_key is the public key of the client. + bytes client_key = 3; +} + +// RequestReservationResponse is a response sent from the server to the client +// to confirm a reservation request. +message RequestReservationResponse { + // reservation_id is the id of the reservation. + bytes reservation_id = 1; + + // server_key is the public key of the server. + bytes server_key = 2; + + // invoice is the invoice for the reservation that the client should pay. + string invoice = 3; + + // expiry is the absolute expiry of the reservation. + uint32 expiry = 4; +} + +// QuoteReservationRequest is a request sent from the client to the server to +// request a quote for a reservation UTXO. +message QuoteReservationRequest { + // value is the value of the reservation in satoshis. + uint64 value = 1; + + // expiry is the relative expiry of the reservation. + uint32 expiry = 2; +} + +// QuoteReservationResponse is a response sent from the server to the client to +// confirm a reservation quote request. +message QuoteReservationResponse { + // prepay_cost is the cost of the prepay. + uint64 prepay_cost = 1; +} // ReservationProtocolVersion is the version of the reservation protocol. enum ReservationProtocolVersion { // RESERVATION_NONE is the default value and means that the reservation diff --git a/swapserverrpc/reservation_grpc.pb.go b/swapserverrpc/reservation_grpc.pb.go index 89deaf706..49585472b 100644 --- a/swapserverrpc/reservation_grpc.pb.go +++ b/swapserverrpc/reservation_grpc.pb.go @@ -24,6 +24,10 @@ type ReservationServiceClient interface { ReservationNotificationStream(ctx context.Context, in *ReservationNotificationRequest, opts ...grpc.CallOption) (ReservationService_ReservationNotificationStreamClient, error) // OpenReservation requests a new reservation UTXO from the server. OpenReservation(ctx context.Context, in *ServerOpenReservationRequest, opts ...grpc.CallOption) (*ServerOpenReservationResponse, error) + // RequestReservation requests a new reservation UTXO from the server. + RequestReservation(ctx context.Context, in *RequestReservationRequest, opts ...grpc.CallOption) (*RequestReservationResponse, error) + // QuoteReservation requests a quote for a reservation UTXO from the server. + QuoteReservation(ctx context.Context, in *QuoteReservationRequest, opts ...grpc.CallOption) (*QuoteReservationResponse, error) } type reservationServiceClient struct { @@ -76,6 +80,24 @@ func (c *reservationServiceClient) OpenReservation(ctx context.Context, in *Serv return out, nil } +func (c *reservationServiceClient) RequestReservation(ctx context.Context, in *RequestReservationRequest, opts ...grpc.CallOption) (*RequestReservationResponse, error) { + out := new(RequestReservationResponse) + err := c.cc.Invoke(ctx, "/looprpc.ReservationService/RequestReservation", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *reservationServiceClient) QuoteReservation(ctx context.Context, in *QuoteReservationRequest, opts ...grpc.CallOption) (*QuoteReservationResponse, error) { + out := new(QuoteReservationResponse) + err := c.cc.Invoke(ctx, "/looprpc.ReservationService/QuoteReservation", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // ReservationServiceServer is the server API for ReservationService service. // All implementations must embed UnimplementedReservationServiceServer // for forward compatibility @@ -86,6 +108,10 @@ type ReservationServiceServer interface { ReservationNotificationStream(*ReservationNotificationRequest, ReservationService_ReservationNotificationStreamServer) error // OpenReservation requests a new reservation UTXO from the server. OpenReservation(context.Context, *ServerOpenReservationRequest) (*ServerOpenReservationResponse, error) + // RequestReservation requests a new reservation UTXO from the server. + RequestReservation(context.Context, *RequestReservationRequest) (*RequestReservationResponse, error) + // QuoteReservation requests a quote for a reservation UTXO from the server. + QuoteReservation(context.Context, *QuoteReservationRequest) (*QuoteReservationResponse, error) mustEmbedUnimplementedReservationServiceServer() } @@ -99,6 +125,12 @@ func (UnimplementedReservationServiceServer) ReservationNotificationStream(*Rese func (UnimplementedReservationServiceServer) OpenReservation(context.Context, *ServerOpenReservationRequest) (*ServerOpenReservationResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method OpenReservation not implemented") } +func (UnimplementedReservationServiceServer) RequestReservation(context.Context, *RequestReservationRequest) (*RequestReservationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RequestReservation not implemented") +} +func (UnimplementedReservationServiceServer) QuoteReservation(context.Context, *QuoteReservationRequest) (*QuoteReservationResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method QuoteReservation not implemented") +} func (UnimplementedReservationServiceServer) mustEmbedUnimplementedReservationServiceServer() {} // UnsafeReservationServiceServer may be embedded to opt out of forward compatibility for this service. @@ -151,6 +183,42 @@ func _ReservationService_OpenReservation_Handler(srv interface{}, ctx context.Co return interceptor(ctx, in, info, handler) } +func _ReservationService_RequestReservation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RequestReservationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReservationServiceServer).RequestReservation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.ReservationService/RequestReservation", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReservationServiceServer).RequestReservation(ctx, req.(*RequestReservationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ReservationService_QuoteReservation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QuoteReservationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ReservationServiceServer).QuoteReservation(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.ReservationService/QuoteReservation", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ReservationServiceServer).QuoteReservation(ctx, req.(*QuoteReservationRequest)) + } + return interceptor(ctx, in, info, handler) +} + // ReservationService_ServiceDesc is the grpc.ServiceDesc for ReservationService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -162,6 +230,14 @@ var ReservationService_ServiceDesc = grpc.ServiceDesc{ MethodName: "OpenReservation", Handler: _ReservationService_OpenReservation_Handler, }, + { + MethodName: "RequestReservation", + Handler: _ReservationService_RequestReservation_Handler, + }, + { + MethodName: "QuoteReservation", + Handler: _ReservationService_QuoteReservation_Handler, + }, }, Streams: []grpc.StreamDesc{ { From 23e533eebc7dda37fb97d76e26d7459413227864 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 3 Feb 2025 14:21:18 +0100 Subject: [PATCH 15/35] reservations: add client requested fsm --- instantout/reservation/actions.go | 181 ++++++++++++++++++++++++- instantout/reservation/actions_test.go | 6 +- instantout/reservation/fsm.go | 100 +++++++++++++- instantout/reservation/reservation.go | 3 + 4 files changed, 278 insertions(+), 12 deletions(-) diff --git a/instantout/reservation/actions.go b/instantout/reservation/actions.go index 9e62c0151..8a805e4d4 100644 --- a/instantout/reservation/actions.go +++ b/instantout/reservation/actions.go @@ -2,16 +2,179 @@ 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 @@ -19,14 +182,14 @@ type InitReservationContext struct { 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) } @@ -240,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) +} diff --git a/instantout/reservation/actions_test.go b/instantout/reservation/actions_test.go index 2120e02e4..e2322a6bb 100644 --- a/instantout/reservation/actions_test.go +++ b/instantout/reservation/actions_test.go @@ -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, @@ -174,7 +174,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) } } diff --git a/instantout/reservation/fsm.go b/instantout/reservation/fsm.go index f61e5097c..46ba40d6d 100644 --- a/instantout/reservation/fsm.go +++ b/instantout/reservation/fsm.go @@ -22,7 +22,11 @@ const ( // ProtocolVersionServerInitiated is the protocol version where the // server initiates the reservation. - ProtocolVersionServerInitiated ProtocolVersion = 0 + ProtocolVersionServerInitiated ProtocolVersion = 1 + + // ProtocolVersionClientInitiated is the protocol version where the + // client initiates the reservation. + ProtocolVersionClientInitiated ProtocolVersion = 2 ) const ( @@ -45,6 +49,12 @@ type Config struct { // swap server. ReservationClient swapserverrpc.ReservationServiceClient + // LightningClient is the lnd client used to handle invoices decoding. + LightningClient lndclient.LightningClient + + // RouterClient is used to send the offchain payments. + RouterClient lndclient.RouterClient + // NotificationManager is the manager that handles the notification // subscriptions. NotificationManager NotificationManager @@ -81,6 +91,10 @@ func NewFSMFromReservation(cfg *Config, reservation *Reservation) *FSM { switch reservation.ProtocolVersion { case ProtocolVersionServerInitiated: states = reservationFsm.GetServerInitiatedReservationStates() + + case ProtocolVersionClientInitiated: + states = reservationFsm.GetClientInitiatedReservationStates() + default: states = make(fsm.States) } @@ -99,6 +113,10 @@ var ( // Init is the initial state of the reservation. Init = fsm.StateType("Init") + // SendPrepaymentPayment is the state where the client sends the payment to the + // server. + SendPrepaymentPayment = fsm.StateType("SendPayment") + // WaitForConfirmation is the state where we wait for the reservation // tx to be confirmed. WaitForConfirmation = fsm.StateType("WaitForConfirmation") @@ -126,6 +144,10 @@ var ( // requests a new reservation. OnServerRequest = fsm.EventType("OnServerRequest") + // OnClientInitialized is the event that is triggered when the client + // has initialized the reservation. + OnClientInitialized = fsm.EventType("OnClientInitialized") + // OnBroadcast is the event that is triggered when the reservation tx // has been broadcast. OnBroadcast = fsm.EventType("OnBroadcast") @@ -159,6 +181,80 @@ var ( OnUnlocked = fsm.EventType("OnUnlocked") ) +// GetClientInitiatedReservationStates returns the statemap that defines the +// reservation state machine, where the client initiates the reservation. +func (f *FSM) GetClientInitiatedReservationStates() fsm.States { + return fsm.States{ + fsm.EmptyState: fsm.State{ + Transitions: fsm.Transitions{ + OnClientInitialized: Init, + }, + Action: nil, + }, + Init: fsm.State{ + Transitions: fsm.Transitions{ + OnClientInitialized: SendPrepaymentPayment, + OnRecover: Failed, + fsm.OnError: Failed, + }, + Action: f.InitFromClientRequestAction, + }, + SendPrepaymentPayment: fsm.State{ + Transitions: fsm.Transitions{ + OnBroadcast: WaitForConfirmation, + OnRecover: SendPrepaymentPayment, + fsm.OnError: Failed, + }, + Action: f.SendPrepayment, + }, + WaitForConfirmation: fsm.State{ + Transitions: fsm.Transitions{ + OnRecover: WaitForConfirmation, + OnConfirmed: Confirmed, + OnTimedOut: TimedOut, + }, + Action: f.SubscribeToConfirmationAction, + }, + Confirmed: fsm.State{ + Transitions: fsm.Transitions{ + OnSpent: Spent, + OnTimedOut: TimedOut, + OnRecover: Confirmed, + OnLocked: Locked, + fsm.OnError: Confirmed, + }, + Action: f.AsyncWaitForExpiredOrSweptAction, + }, + Locked: fsm.State{ + Transitions: fsm.Transitions{ + OnUnlocked: Confirmed, + OnTimedOut: TimedOut, + OnRecover: Locked, + OnSpent: Spent, + fsm.OnError: Locked, + }, + Action: f.AsyncWaitForExpiredOrSweptAction, + }, + TimedOut: fsm.State{ + Transitions: fsm.Transitions{ + OnTimedOut: TimedOut, + }, + Action: fsm.NoOpAction, + }, + + Spent: fsm.State{ + Transitions: fsm.Transitions{ + OnSpent: Spent, + }, + Action: fsm.NoOpAction, + }, + + Failed: fsm.State{ + Action: fsm.NoOpAction, + }, + } +} + // GetServerInitiatedReservationStates returns the statemap that defines the // reservation state machine, where the server initiates the reservation. func (f *FSM) GetServerInitiatedReservationStates() fsm.States { @@ -175,7 +271,7 @@ func (f *FSM) GetServerInitiatedReservationStates() fsm.States { OnRecover: Failed, fsm.OnError: Failed, }, - Action: f.InitAction, + Action: f.InitFromServerRequestAction, }, WaitForConfirmation: fsm.State{ Transitions: fsm.Transitions{ diff --git a/instantout/reservation/reservation.go b/instantout/reservation/reservation.go index 8b83ae33c..e450ecb1d 100644 --- a/instantout/reservation/reservation.go +++ b/instantout/reservation/reservation.go @@ -62,6 +62,9 @@ type Reservation struct { // Outpoint is the outpoint of the reservation. Outpoint *wire.OutPoint + // PrepayInvoice is the invoice that the client paid as a prepayment. + PrepayInvoice string + // InitiationHeight is the height at which the reservation was // initiated. InitiationHeight int32 From 4f121287d6c41d68ba3eaa2c0faed03f64ce862e Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 3 Feb 2025 16:14:04 +0100 Subject: [PATCH 16/35] reservation: add client requested reservations to manager --- instantout/reservation/manager.go | 110 +++++++++++++++++++++++-- instantout/reservation/manager_test.go | 2 +- 2 files changed, 103 insertions(+), 9 deletions(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 78251a554..42d163fb9 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -11,9 +11,21 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/swapserverrpc" reservationrpc "github.com/lightninglabs/loop/swapserverrpc" ) +var ( + defaultWaitForStateTime = time.Second * 15 +) + +// FSMSendEventReq contains the information needed to send an event to the FSM. +type FSMSendEventReq struct { + fsm *FSM + event fsm.EventType + eventCtx fsm.EventContext +} + // Manager manages the reservation state machines. type Manager struct { sync.Mutex @@ -24,6 +36,10 @@ type Manager struct { // activeReservations contains all the active reservationsFSMs. activeReservations map[ID]*FSM + + currentHeight int32 + + reqChan chan *FSMSendEventReq } // finalStateObserver removes a reservation FSM from the active set once it @@ -53,6 +69,7 @@ func NewManager(cfg *Config) *Manager { return &Manager{ cfg: cfg, activeReservations: make(map[ID]*FSM), + reqChan: make(chan *FSMSendEventReq), } } @@ -65,7 +82,7 @@ func (m *Manager) Run(ctx context.Context, height int32, runCtx, cancel := context.WithCancel(ctx) defer cancel() - currentHeight := height + m.currentHeight = height err := m.RecoverReservations(runCtx) if err != nil { @@ -87,7 +104,9 @@ func (m *Manager) Run(ctx context.Context, height int32, select { case height := <-newBlockChan: log.Debugf("Received block %v", height) - currentHeight = height + m.Lock() + m.currentHeight = height + m.Unlock() case reservationRes, ok := <-ntfnChan: if !ok { @@ -99,14 +118,27 @@ func (m *Manager) Run(ctx context.Context, height int32, log.Debugf("Received reservation %x", reservationRes.ReservationId) - _, err := m.newReservation( - runCtx, uint32(currentHeight), reservationRes, + _, err := m.newReservationFromNtfn( + runCtx, uint32(m.currentHeight), reservationRes, ) if err != nil { log.Errorf("Unable to create reservation %x: %v", reservationRes.ReservationId, err) } + case req := <-m.reqChan: + // We'll send the event in a goroutine to avoid blocking + // the main loop. + go func() { + err := req.fsm.SendEvent( + runCtx, req.event, req.eventCtx, + ) + if err != nil { + log.Errorf("Error sending event: %v", + err) + } + }() + case err := <-newBlockErrChan: return err @@ -117,9 +149,11 @@ func (m *Manager) Run(ctx context.Context, height int32, } } -// newReservation creates a new reservation from the reservation request. -func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, - req *reservationrpc.ServerReservationNotification) (*FSM, error) { +// newReservationFromNtfn creates a new reservation from the reservation +// notification. +func (m *Manager) newReservationFromNtfn(ctx context.Context, + currentHeight uint32, req *reservationrpc.ServerReservationNotification, +) (*FSM, error) { var reservationID ID err := reservationID.FromByteSlice( @@ -169,7 +203,7 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, fsm: reservationFSM, }) - initContext := &InitReservationContext{ + initContext := &ServerRequestedInitContext{ reservationID: reservationID, serverPubkey: serverKey, value: btcutil.Amount(req.Value), @@ -211,6 +245,66 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, return reservationFSM, nil } +// RequestReservationFromServer sends a request to the server to create a new +// reservation. +func (m *Manager) RequestReservationFromServer(ctx context.Context, + value btcutil.Amount, expiry uint32, maxPrepaymentAmt btcutil.Amount) ( + *Reservation, error) { + + m.Lock() + currentHeight := m.currentHeight + m.Unlock() + // Create a new reservation req. + req := &ClientRequestedInitContext{ + value: value, + relativeExpiry: expiry, + heightHint: uint32(currentHeight), + maxPrepaymentAmt: maxPrepaymentAmt, + } + + reservationFSM := NewFSM(m.cfg, ProtocolVersionClientInitiated) + // Send the event to the main loop. + m.reqChan <- &FSMSendEventReq{ + fsm: reservationFSM, + event: OnClientInitialized, + eventCtx: req, + } + + // We'll now wait for the reservation to be in the state where we are + // sending the prepayment. + err := reservationFSM.DefaultObserver.WaitForState( + ctx, defaultWaitForStateTime, SendPrepaymentPayment, + fsm.WithAbortEarlyOnErrorOption(), + ) + if err != nil { + return nil, err + } + + // Now we can add the reservation to our active fsm. + m.Lock() + m.activeReservations[reservationFSM.reservation.ID] = reservationFSM + m.Unlock() + + return reservationFSM.reservation, nil +} + +// QuoteReservation quotes the server for a new reservation. +func (m *Manager) QuoteReservation(ctx context.Context, value btcutil.Amount, + expiry uint32) (btcutil.Amount, error) { + + quoteReq := &swapserverrpc.QuoteReservationRequest{ + Value: uint64(value), + Expiry: expiry, + } + + req, err := m.cfg.ReservationClient.QuoteReservation(ctx, quoteReq) + if err != nil { + return 0, err + } + + return btcutil.Amount(req.PrepayCost), nil +} + // RecoverReservations tries to recover all reservations that are still active // from the database. func (m *Manager) RecoverReservations(ctx context.Context) error { diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index e04929c31..fd7db24b3 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -36,7 +36,7 @@ func TestManager(t *testing.T) { <-initChan // Create a new reservation. - reservationFSM, err := testContext.manager.newReservation( + reservationFSM, err := testContext.manager.newReservationFromNtfn( ctxb, uint32(testContext.mockLnd.Height), &swapserverrpc.ServerReservationNotification{ ReservationId: defaultReservationId[:], From 327d4b42d0d4e17e6b962ea00ddda38bd05b61f5 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 3 Feb 2025 17:18:07 +0100 Subject: [PATCH 17/35] loopdb: store reservation prepay invoice --- instantout/reservation/store.go | 2 ++ .../000015_reservation_prepay_invoice.down.sql | 1 + .../000015_reservation_prepay_invoice.up.sql | 3 +++ loopdb/sqlc/models.go | 1 + loopdb/sqlc/queries/reservations.sql | 6 ++++-- loopdb/sqlc/reservations.sql.go | 14 ++++++++++---- 6 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 loopdb/sqlc/migrations/000015_reservation_prepay_invoice.down.sql create mode 100644 loopdb/sqlc/migrations/000015_reservation_prepay_invoice.up.sql diff --git a/instantout/reservation/store.go b/instantout/reservation/store.go index 72613f9ca..dabade502 100644 --- a/instantout/reservation/store.go +++ b/instantout/reservation/store.go @@ -84,6 +84,7 @@ func (r *SQLStore) CreateReservation(ctx context.Context, ClientKeyIndex: int32(reservation.KeyLocator.Index), InitiationHeight: reservation.InitiationHeight, ProtocolVersion: int32(reservation.ProtocolVersion), + PrepayInvoice: reservation.PrepayInvoice, } updateArgs := sqlc.InsertReservationUpdateParams{ @@ -293,6 +294,7 @@ func sqlReservationToReservation(row sqlc.Reservation, InitiationHeight: row.InitiationHeight, State: fsm.StateType(lastUpdate.UpdateState), ProtocolVersion: ProtocolVersion(row.ProtocolVersion), + PrepayInvoice: row.PrepayInvoice, }, nil } diff --git a/loopdb/sqlc/migrations/000015_reservation_prepay_invoice.down.sql b/loopdb/sqlc/migrations/000015_reservation_prepay_invoice.down.sql new file mode 100644 index 000000000..61ae44c69 --- /dev/null +++ b/loopdb/sqlc/migrations/000015_reservation_prepay_invoice.down.sql @@ -0,0 +1 @@ +ALTER TABLE reservations DROP COLUMN prepay_invoice; diff --git a/loopdb/sqlc/migrations/000015_reservation_prepay_invoice.up.sql b/loopdb/sqlc/migrations/000015_reservation_prepay_invoice.up.sql new file mode 100644 index 000000000..37075170f --- /dev/null +++ b/loopdb/sqlc/migrations/000015_reservation_prepay_invoice.up.sql @@ -0,0 +1,3 @@ +-- prepay_invoice is a field that will store the invoice of the prepay payment +-- that pays for the reservation. +ALTER TABLE reservations ADD COLUMN prepay_invoice TEXT NOT NULL DEFAULT ''; diff --git a/loopdb/sqlc/models.go b/loopdb/sqlc/models.go index 78a75d042..62bf5a66c 100644 --- a/loopdb/sqlc/models.go +++ b/loopdb/sqlc/models.go @@ -115,6 +115,7 @@ type Reservation struct { OutIndex sql.NullInt32 ConfirmationHeight sql.NullInt32 ProtocolVersion int32 + PrepayInvoice string } type ReservationUpdate struct { diff --git a/loopdb/sqlc/queries/reservations.sql b/loopdb/sqlc/queries/reservations.sql index ba95f53a7..f95923260 100644 --- a/loopdb/sqlc/queries/reservations.sql +++ b/loopdb/sqlc/queries/reservations.sql @@ -8,7 +8,8 @@ INSERT INTO reservations ( client_key_family, client_key_index, initiation_height, - protocol_version + protocol_version, + prepay_invoice ) VALUES ( $1, $2, @@ -18,7 +19,8 @@ INSERT INTO reservations ( $6, $7, $8, - $9 + $9, + $10 ); -- name: UpdateReservation :exec diff --git a/loopdb/sqlc/reservations.sql.go b/loopdb/sqlc/reservations.sql.go index 3a1ba8987..275f45b83 100644 --- a/loopdb/sqlc/reservations.sql.go +++ b/loopdb/sqlc/reservations.sql.go @@ -21,7 +21,8 @@ INSERT INTO reservations ( client_key_family, client_key_index, initiation_height, - protocol_version + protocol_version, + prepay_invoice ) VALUES ( $1, $2, @@ -31,7 +32,8 @@ INSERT INTO reservations ( $6, $7, $8, - $9 + $9, + $10 ) ` @@ -45,6 +47,7 @@ type CreateReservationParams struct { ClientKeyIndex int32 InitiationHeight int32 ProtocolVersion int32 + PrepayInvoice string } func (q *Queries) CreateReservation(ctx context.Context, arg CreateReservationParams) error { @@ -58,13 +61,14 @@ func (q *Queries) CreateReservation(ctx context.Context, arg CreateReservationPa arg.ClientKeyIndex, arg.InitiationHeight, arg.ProtocolVersion, + arg.PrepayInvoice, ) return err } const getReservation = `-- name: GetReservation :one SELECT - id, reservation_id, client_pubkey, server_pubkey, expiry, value, client_key_family, client_key_index, initiation_height, tx_hash, out_index, confirmation_height, protocol_version + id, reservation_id, client_pubkey, server_pubkey, expiry, value, client_key_family, client_key_index, initiation_height, tx_hash, out_index, confirmation_height, protocol_version, prepay_invoice FROM reservations WHERE @@ -88,6 +92,7 @@ func (q *Queries) GetReservation(ctx context.Context, reservationID []byte) (Res &i.OutIndex, &i.ConfirmationHeight, &i.ProtocolVersion, + &i.PrepayInvoice, ) return i, err } @@ -133,7 +138,7 @@ func (q *Queries) GetReservationUpdates(ctx context.Context, reservationID []byt const getReservations = `-- name: GetReservations :many SELECT - id, reservation_id, client_pubkey, server_pubkey, expiry, value, client_key_family, client_key_index, initiation_height, tx_hash, out_index, confirmation_height, protocol_version + id, reservation_id, client_pubkey, server_pubkey, expiry, value, client_key_family, client_key_index, initiation_height, tx_hash, out_index, confirmation_height, protocol_version, prepay_invoice FROM reservations ORDER BY @@ -163,6 +168,7 @@ func (q *Queries) GetReservations(ctx context.Context) ([]Reservation, error) { &i.OutIndex, &i.ConfirmationHeight, &i.ProtocolVersion, + &i.PrepayInvoice, ); err != nil { return nil, err } From 5ca0ccd4869e8d2d819020dc605aa01d80bb9260 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 3 Feb 2025 16:14:31 +0100 Subject: [PATCH 18/35] loopd: update reservation cfg --- loopd/daemon.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/loopd/daemon.go b/loopd/daemon.go index 319709d84..055721bd3 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -761,6 +761,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error { ChainNotifier: d.lnd.ChainNotifier, ReservationClient: reservationClient, NotificationManager: notificationManager, + LightningClient: d.lnd.Client, + RouterClient: d.lnd.Router, } reservationManager = reservation.NewManager( From 04a874e0e17f7844f2aad466b58ece1fd51257b1 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 3 Feb 2025 16:13:02 +0100 Subject: [PATCH 19/35] looprpc: add client calls --- looprpc/client.pb.go | 691 ++++++++++++++++++++++------------ looprpc/client.proto | 55 ++- looprpc/client.swagger.json | 20 +- looprpc/client_grpc.pb.go | 86 ++++- looprpc/perms.go | 8 + looprpc/swapclient.pb.json.go | 50 +++ 6 files changed, 675 insertions(+), 235 deletions(-) diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index 53fb113f9..882efd771 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -4457,6 +4457,212 @@ func (x *ClientReservation) GetExpiry() uint32 { return 0 } +type ReservationRequestRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The amount to reserve in satoshis. + Amt uint64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"` + // The relative expiry of the reservation in blocks. + Expiry uint32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"` + // The maximum amt in satoshis we allow for the prepayment. + MaxPrepayAmt uint64 `protobuf:"varint,3,opt,name=max_prepay_amt,json=maxPrepayAmt,proto3" json:"max_prepay_amt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReservationRequestRequest) Reset() { + *x = ReservationRequestRequest{} + mi := &file_client_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReservationRequestRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReservationRequestRequest) ProtoMessage() {} + +func (x *ReservationRequestRequest) ProtoReflect() protoreflect.Message { + mi := &file_client_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReservationRequestRequest.ProtoReflect.Descriptor instead. +func (*ReservationRequestRequest) Descriptor() ([]byte, []int) { + return file_client_proto_rawDescGZIP(), []int{48} +} + +func (x *ReservationRequestRequest) GetAmt() uint64 { + if x != nil { + return x.Amt + } + return 0 +} + +func (x *ReservationRequestRequest) GetExpiry() uint32 { + if x != nil { + return x.Expiry + } + return 0 +} + +func (x *ReservationRequestRequest) GetMaxPrepayAmt() uint64 { + if x != nil { + return x.MaxPrepayAmt + } + return 0 +} + +type ReservationRequestResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Reservation *ClientReservation `protobuf:"bytes,1,opt,name=reservation,proto3" json:"reservation,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReservationRequestResponse) Reset() { + *x = ReservationRequestResponse{} + mi := &file_client_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReservationRequestResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReservationRequestResponse) ProtoMessage() {} + +func (x *ReservationRequestResponse) ProtoReflect() protoreflect.Message { + mi := &file_client_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReservationRequestResponse.ProtoReflect.Descriptor instead. +func (*ReservationRequestResponse) Descriptor() ([]byte, []int) { + return file_client_proto_rawDescGZIP(), []int{49} +} + +func (x *ReservationRequestResponse) GetReservation() *ClientReservation { + if x != nil { + return x.Reservation + } + return nil +} + +type ReservationQuoteRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The amount to reserve in satoshis. + Amt uint64 `protobuf:"varint,1,opt,name=amt,proto3" json:"amt,omitempty"` + // The relative expiry of the reservation in blocks. + Expiry uint32 `protobuf:"varint,2,opt,name=expiry,proto3" json:"expiry,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReservationQuoteRequest) Reset() { + *x = ReservationQuoteRequest{} + mi := &file_client_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReservationQuoteRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReservationQuoteRequest) ProtoMessage() {} + +func (x *ReservationQuoteRequest) ProtoReflect() protoreflect.Message { + mi := &file_client_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReservationQuoteRequest.ProtoReflect.Descriptor instead. +func (*ReservationQuoteRequest) Descriptor() ([]byte, []int) { + return file_client_proto_rawDescGZIP(), []int{50} +} + +func (x *ReservationQuoteRequest) GetAmt() uint64 { + if x != nil { + return x.Amt + } + return 0 +} + +func (x *ReservationQuoteRequest) GetExpiry() uint32 { + if x != nil { + return x.Expiry + } + return 0 +} + +type ReservationQuoteResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The prepay fee that will be charged for the reservation. + PrepayAmt uint64 `protobuf:"varint,1,opt,name=prepay_amt,json=prepayAmt,proto3" json:"prepay_amt,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReservationQuoteResponse) Reset() { + *x = ReservationQuoteResponse{} + mi := &file_client_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReservationQuoteResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReservationQuoteResponse) ProtoMessage() {} + +func (x *ReservationQuoteResponse) ProtoReflect() protoreflect.Message { + mi := &file_client_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReservationQuoteResponse.ProtoReflect.Descriptor instead. +func (*ReservationQuoteResponse) Descriptor() ([]byte, []int) { + return file_client_proto_rawDescGZIP(), []int{51} +} + +func (x *ReservationQuoteResponse) GetPrepayAmt() uint64 { + if x != nil { + return x.PrepayAmt + } + return 0 +} + type InstantOutRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The reservations to use for the swap. @@ -4476,7 +4682,7 @@ type InstantOutRequest struct { func (x *InstantOutRequest) Reset() { *x = InstantOutRequest{} - mi := &file_client_proto_msgTypes[48] + mi := &file_client_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4488,7 +4694,7 @@ func (x *InstantOutRequest) String() string { func (*InstantOutRequest) ProtoMessage() {} func (x *InstantOutRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[48] + mi := &file_client_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4501,7 +4707,7 @@ func (x *InstantOutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOutRequest.ProtoReflect.Descriptor instead. func (*InstantOutRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{48} + return file_client_proto_rawDescGZIP(), []int{52} } func (x *InstantOutRequest) GetReservationIds() [][]byte { @@ -4546,7 +4752,7 @@ type InstantOutResponse struct { func (x *InstantOutResponse) Reset() { *x = InstantOutResponse{} - mi := &file_client_proto_msgTypes[49] + mi := &file_client_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4558,7 +4764,7 @@ func (x *InstantOutResponse) String() string { func (*InstantOutResponse) ProtoMessage() {} func (x *InstantOutResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[49] + mi := &file_client_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4571,7 +4777,7 @@ func (x *InstantOutResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOutResponse.ProtoReflect.Descriptor instead. func (*InstantOutResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{49} + return file_client_proto_rawDescGZIP(), []int{53} } func (x *InstantOutResponse) GetInstantOutHash() []byte { @@ -4612,7 +4818,7 @@ type InstantOutQuoteRequest struct { func (x *InstantOutQuoteRequest) Reset() { *x = InstantOutQuoteRequest{} - mi := &file_client_proto_msgTypes[50] + mi := &file_client_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4624,7 +4830,7 @@ func (x *InstantOutQuoteRequest) String() string { func (*InstantOutQuoteRequest) ProtoMessage() {} func (x *InstantOutQuoteRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[50] + mi := &file_client_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4637,7 +4843,7 @@ func (x *InstantOutQuoteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOutQuoteRequest.ProtoReflect.Descriptor instead. func (*InstantOutQuoteRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{50} + return file_client_proto_rawDescGZIP(), []int{54} } func (x *InstantOutQuoteRequest) GetAmt() uint64 { @@ -4675,7 +4881,7 @@ type InstantOutQuoteResponse struct { func (x *InstantOutQuoteResponse) Reset() { *x = InstantOutQuoteResponse{} - mi := &file_client_proto_msgTypes[51] + mi := &file_client_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4687,7 +4893,7 @@ func (x *InstantOutQuoteResponse) String() string { func (*InstantOutQuoteResponse) ProtoMessage() {} func (x *InstantOutQuoteResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[51] + mi := &file_client_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4700,7 +4906,7 @@ func (x *InstantOutQuoteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOutQuoteResponse.ProtoReflect.Descriptor instead. func (*InstantOutQuoteResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{51} + return file_client_proto_rawDescGZIP(), []int{55} } func (x *InstantOutQuoteResponse) GetServiceFeeSat() int64 { @@ -4725,7 +4931,7 @@ type ListInstantOutsRequest struct { func (x *ListInstantOutsRequest) Reset() { *x = ListInstantOutsRequest{} - mi := &file_client_proto_msgTypes[52] + mi := &file_client_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4737,7 +4943,7 @@ func (x *ListInstantOutsRequest) String() string { func (*ListInstantOutsRequest) ProtoMessage() {} func (x *ListInstantOutsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[52] + mi := &file_client_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4750,7 +4956,7 @@ func (x *ListInstantOutsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstantOutsRequest.ProtoReflect.Descriptor instead. func (*ListInstantOutsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{52} + return file_client_proto_rawDescGZIP(), []int{56} } type ListInstantOutsResponse struct { @@ -4763,7 +4969,7 @@ type ListInstantOutsResponse struct { func (x *ListInstantOutsResponse) Reset() { *x = ListInstantOutsResponse{} - mi := &file_client_proto_msgTypes[53] + mi := &file_client_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4775,7 +4981,7 @@ func (x *ListInstantOutsResponse) String() string { func (*ListInstantOutsResponse) ProtoMessage() {} func (x *ListInstantOutsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[53] + mi := &file_client_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4788,7 +4994,7 @@ func (x *ListInstantOutsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListInstantOutsResponse.ProtoReflect.Descriptor instead. func (*ListInstantOutsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{53} + return file_client_proto_rawDescGZIP(), []int{57} } func (x *ListInstantOutsResponse) GetSwaps() []*InstantOut { @@ -4816,7 +5022,7 @@ type InstantOut struct { func (x *InstantOut) Reset() { *x = InstantOut{} - mi := &file_client_proto_msgTypes[54] + mi := &file_client_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4828,7 +5034,7 @@ func (x *InstantOut) String() string { func (*InstantOut) ProtoMessage() {} func (x *InstantOut) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[54] + mi := &file_client_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4841,7 +5047,7 @@ func (x *InstantOut) ProtoReflect() protoreflect.Message { // Deprecated: Use InstantOut.ProtoReflect.Descriptor instead. func (*InstantOut) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{54} + return file_client_proto_rawDescGZIP(), []int{58} } func (x *InstantOut) GetSwapHash() []byte { @@ -4889,7 +5095,7 @@ type NewStaticAddressRequest struct { func (x *NewStaticAddressRequest) Reset() { *x = NewStaticAddressRequest{} - mi := &file_client_proto_msgTypes[55] + mi := &file_client_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4901,7 +5107,7 @@ func (x *NewStaticAddressRequest) String() string { func (*NewStaticAddressRequest) ProtoMessage() {} func (x *NewStaticAddressRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[55] + mi := &file_client_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4914,7 +5120,7 @@ func (x *NewStaticAddressRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use NewStaticAddressRequest.ProtoReflect.Descriptor instead. func (*NewStaticAddressRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{55} + return file_client_proto_rawDescGZIP(), []int{59} } func (x *NewStaticAddressRequest) GetClientKey() []byte { @@ -4936,7 +5142,7 @@ type NewStaticAddressResponse struct { func (x *NewStaticAddressResponse) Reset() { *x = NewStaticAddressResponse{} - mi := &file_client_proto_msgTypes[56] + mi := &file_client_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4948,7 +5154,7 @@ func (x *NewStaticAddressResponse) String() string { func (*NewStaticAddressResponse) ProtoMessage() {} func (x *NewStaticAddressResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[56] + mi := &file_client_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4961,7 +5167,7 @@ func (x *NewStaticAddressResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use NewStaticAddressResponse.ProtoReflect.Descriptor instead. func (*NewStaticAddressResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{56} + return file_client_proto_rawDescGZIP(), []int{60} } func (x *NewStaticAddressResponse) GetAddress() string { @@ -4991,7 +5197,7 @@ type ListUnspentDepositsRequest struct { func (x *ListUnspentDepositsRequest) Reset() { *x = ListUnspentDepositsRequest{} - mi := &file_client_proto_msgTypes[57] + mi := &file_client_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5003,7 +5209,7 @@ func (x *ListUnspentDepositsRequest) String() string { func (*ListUnspentDepositsRequest) ProtoMessage() {} func (x *ListUnspentDepositsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[57] + mi := &file_client_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5016,7 +5222,7 @@ func (x *ListUnspentDepositsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUnspentDepositsRequest.ProtoReflect.Descriptor instead. func (*ListUnspentDepositsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{57} + return file_client_proto_rawDescGZIP(), []int{61} } func (x *ListUnspentDepositsRequest) GetMinConfs() int32 { @@ -5043,7 +5249,7 @@ type ListUnspentDepositsResponse struct { func (x *ListUnspentDepositsResponse) Reset() { *x = ListUnspentDepositsResponse{} - mi := &file_client_proto_msgTypes[58] + mi := &file_client_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5055,7 +5261,7 @@ func (x *ListUnspentDepositsResponse) String() string { func (*ListUnspentDepositsResponse) ProtoMessage() {} func (x *ListUnspentDepositsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[58] + mi := &file_client_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5068,7 +5274,7 @@ func (x *ListUnspentDepositsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListUnspentDepositsResponse.ProtoReflect.Descriptor instead. func (*ListUnspentDepositsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{58} + return file_client_proto_rawDescGZIP(), []int{62} } func (x *ListUnspentDepositsResponse) GetUtxos() []*Utxo { @@ -5094,7 +5300,7 @@ type Utxo struct { func (x *Utxo) Reset() { *x = Utxo{} - mi := &file_client_proto_msgTypes[59] + mi := &file_client_proto_msgTypes[63] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5106,7 +5312,7 @@ func (x *Utxo) String() string { func (*Utxo) ProtoMessage() {} func (x *Utxo) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[59] + mi := &file_client_proto_msgTypes[63] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5119,7 +5325,7 @@ func (x *Utxo) ProtoReflect() protoreflect.Message { // Deprecated: Use Utxo.ProtoReflect.Descriptor instead. func (*Utxo) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{59} + return file_client_proto_rawDescGZIP(), []int{63} } func (x *Utxo) GetStaticAddress() string { @@ -5172,7 +5378,7 @@ type WithdrawDepositsRequest struct { func (x *WithdrawDepositsRequest) Reset() { *x = WithdrawDepositsRequest{} - mi := &file_client_proto_msgTypes[60] + mi := &file_client_proto_msgTypes[64] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5184,7 +5390,7 @@ func (x *WithdrawDepositsRequest) String() string { func (*WithdrawDepositsRequest) ProtoMessage() {} func (x *WithdrawDepositsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[60] + mi := &file_client_proto_msgTypes[64] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5197,7 +5403,7 @@ func (x *WithdrawDepositsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use WithdrawDepositsRequest.ProtoReflect.Descriptor instead. func (*WithdrawDepositsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{60} + return file_client_proto_rawDescGZIP(), []int{64} } func (x *WithdrawDepositsRequest) GetOutpoints() []*lnrpc.OutPoint { @@ -5247,7 +5453,7 @@ type WithdrawDepositsResponse struct { func (x *WithdrawDepositsResponse) Reset() { *x = WithdrawDepositsResponse{} - mi := &file_client_proto_msgTypes[61] + mi := &file_client_proto_msgTypes[65] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5259,7 +5465,7 @@ func (x *WithdrawDepositsResponse) String() string { func (*WithdrawDepositsResponse) ProtoMessage() {} func (x *WithdrawDepositsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[61] + mi := &file_client_proto_msgTypes[65] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5272,7 +5478,7 @@ func (x *WithdrawDepositsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use WithdrawDepositsResponse.ProtoReflect.Descriptor instead. func (*WithdrawDepositsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{61} + return file_client_proto_rawDescGZIP(), []int{65} } func (x *WithdrawDepositsResponse) GetWithdrawalTxHash() string { @@ -5301,7 +5507,7 @@ type ListStaticAddressDepositsRequest struct { func (x *ListStaticAddressDepositsRequest) Reset() { *x = ListStaticAddressDepositsRequest{} - mi := &file_client_proto_msgTypes[62] + mi := &file_client_proto_msgTypes[66] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5313,7 +5519,7 @@ func (x *ListStaticAddressDepositsRequest) String() string { func (*ListStaticAddressDepositsRequest) ProtoMessage() {} func (x *ListStaticAddressDepositsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[62] + mi := &file_client_proto_msgTypes[66] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5326,7 +5532,7 @@ func (x *ListStaticAddressDepositsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStaticAddressDepositsRequest.ProtoReflect.Descriptor instead. func (*ListStaticAddressDepositsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{62} + return file_client_proto_rawDescGZIP(), []int{66} } func (x *ListStaticAddressDepositsRequest) GetStateFilter() DepositState { @@ -5353,7 +5559,7 @@ type ListStaticAddressDepositsResponse struct { func (x *ListStaticAddressDepositsResponse) Reset() { *x = ListStaticAddressDepositsResponse{} - mi := &file_client_proto_msgTypes[63] + mi := &file_client_proto_msgTypes[67] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5365,7 +5571,7 @@ func (x *ListStaticAddressDepositsResponse) String() string { func (*ListStaticAddressDepositsResponse) ProtoMessage() {} func (x *ListStaticAddressDepositsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[63] + mi := &file_client_proto_msgTypes[67] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5378,7 +5584,7 @@ func (x *ListStaticAddressDepositsResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListStaticAddressDepositsResponse.ProtoReflect.Descriptor instead. func (*ListStaticAddressDepositsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{63} + return file_client_proto_rawDescGZIP(), []int{67} } func (x *ListStaticAddressDepositsResponse) GetFilteredDeposits() []*Deposit { @@ -5396,7 +5602,7 @@ type ListStaticAddressWithdrawalRequest struct { func (x *ListStaticAddressWithdrawalRequest) Reset() { *x = ListStaticAddressWithdrawalRequest{} - mi := &file_client_proto_msgTypes[64] + mi := &file_client_proto_msgTypes[68] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5408,7 +5614,7 @@ func (x *ListStaticAddressWithdrawalRequest) String() string { func (*ListStaticAddressWithdrawalRequest) ProtoMessage() {} func (x *ListStaticAddressWithdrawalRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[64] + mi := &file_client_proto_msgTypes[68] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5421,7 +5627,7 @@ func (x *ListStaticAddressWithdrawalRequest) ProtoReflect() protoreflect.Message // Deprecated: Use ListStaticAddressWithdrawalRequest.ProtoReflect.Descriptor instead. func (*ListStaticAddressWithdrawalRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{64} + return file_client_proto_rawDescGZIP(), []int{68} } type ListStaticAddressWithdrawalResponse struct { @@ -5434,7 +5640,7 @@ type ListStaticAddressWithdrawalResponse struct { func (x *ListStaticAddressWithdrawalResponse) Reset() { *x = ListStaticAddressWithdrawalResponse{} - mi := &file_client_proto_msgTypes[65] + mi := &file_client_proto_msgTypes[69] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5446,7 +5652,7 @@ func (x *ListStaticAddressWithdrawalResponse) String() string { func (*ListStaticAddressWithdrawalResponse) ProtoMessage() {} func (x *ListStaticAddressWithdrawalResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[65] + mi := &file_client_proto_msgTypes[69] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5459,7 +5665,7 @@ func (x *ListStaticAddressWithdrawalResponse) ProtoReflect() protoreflect.Messag // Deprecated: Use ListStaticAddressWithdrawalResponse.ProtoReflect.Descriptor instead. func (*ListStaticAddressWithdrawalResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{65} + return file_client_proto_rawDescGZIP(), []int{69} } func (x *ListStaticAddressWithdrawalResponse) GetWithdrawals() []*StaticAddressWithdrawal { @@ -5477,7 +5683,7 @@ type ListStaticAddressSwapsRequest struct { func (x *ListStaticAddressSwapsRequest) Reset() { *x = ListStaticAddressSwapsRequest{} - mi := &file_client_proto_msgTypes[66] + mi := &file_client_proto_msgTypes[70] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5489,7 +5695,7 @@ func (x *ListStaticAddressSwapsRequest) String() string { func (*ListStaticAddressSwapsRequest) ProtoMessage() {} func (x *ListStaticAddressSwapsRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[66] + mi := &file_client_proto_msgTypes[70] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5502,7 +5708,7 @@ func (x *ListStaticAddressSwapsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStaticAddressSwapsRequest.ProtoReflect.Descriptor instead. func (*ListStaticAddressSwapsRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{66} + return file_client_proto_rawDescGZIP(), []int{70} } type ListStaticAddressSwapsResponse struct { @@ -5515,7 +5721,7 @@ type ListStaticAddressSwapsResponse struct { func (x *ListStaticAddressSwapsResponse) Reset() { *x = ListStaticAddressSwapsResponse{} - mi := &file_client_proto_msgTypes[67] + mi := &file_client_proto_msgTypes[71] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5527,7 +5733,7 @@ func (x *ListStaticAddressSwapsResponse) String() string { func (*ListStaticAddressSwapsResponse) ProtoMessage() {} func (x *ListStaticAddressSwapsResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[67] + mi := &file_client_proto_msgTypes[71] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5540,7 +5746,7 @@ func (x *ListStaticAddressSwapsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListStaticAddressSwapsResponse.ProtoReflect.Descriptor instead. func (*ListStaticAddressSwapsResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{67} + return file_client_proto_rawDescGZIP(), []int{71} } func (x *ListStaticAddressSwapsResponse) GetSwaps() []*StaticAddressLoopInSwap { @@ -5558,7 +5764,7 @@ type StaticAddressSummaryRequest struct { func (x *StaticAddressSummaryRequest) Reset() { *x = StaticAddressSummaryRequest{} - mi := &file_client_proto_msgTypes[68] + mi := &file_client_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5570,7 +5776,7 @@ func (x *StaticAddressSummaryRequest) String() string { func (*StaticAddressSummaryRequest) ProtoMessage() {} func (x *StaticAddressSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[68] + mi := &file_client_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5583,7 +5789,7 @@ func (x *StaticAddressSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressSummaryRequest.ProtoReflect.Descriptor instead. func (*StaticAddressSummaryRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{68} + return file_client_proto_rawDescGZIP(), []int{72} } type StaticAddressSummaryResponse struct { @@ -5614,7 +5820,7 @@ type StaticAddressSummaryResponse struct { func (x *StaticAddressSummaryResponse) Reset() { *x = StaticAddressSummaryResponse{} - mi := &file_client_proto_msgTypes[69] + mi := &file_client_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5626,7 +5832,7 @@ func (x *StaticAddressSummaryResponse) String() string { func (*StaticAddressSummaryResponse) ProtoMessage() {} func (x *StaticAddressSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[69] + mi := &file_client_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5639,7 +5845,7 @@ func (x *StaticAddressSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressSummaryResponse.ProtoReflect.Descriptor instead. func (*StaticAddressSummaryResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{69} + return file_client_proto_rawDescGZIP(), []int{73} } func (x *StaticAddressSummaryResponse) GetStaticAddress() string { @@ -5736,7 +5942,7 @@ type Deposit struct { func (x *Deposit) Reset() { *x = Deposit{} - mi := &file_client_proto_msgTypes[70] + mi := &file_client_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5748,7 +5954,7 @@ func (x *Deposit) String() string { func (*Deposit) ProtoMessage() {} func (x *Deposit) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[70] + mi := &file_client_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5761,7 +5967,7 @@ func (x *Deposit) ProtoReflect() protoreflect.Message { // Deprecated: Use Deposit.ProtoReflect.Descriptor instead. func (*Deposit) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{70} + return file_client_proto_rawDescGZIP(), []int{74} } func (x *Deposit) GetId() []byte { @@ -5835,7 +6041,7 @@ type StaticAddressWithdrawal struct { func (x *StaticAddressWithdrawal) Reset() { *x = StaticAddressWithdrawal{} - mi := &file_client_proto_msgTypes[71] + mi := &file_client_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5847,7 +6053,7 @@ func (x *StaticAddressWithdrawal) String() string { func (*StaticAddressWithdrawal) ProtoMessage() {} func (x *StaticAddressWithdrawal) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[71] + mi := &file_client_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5860,7 +6066,7 @@ func (x *StaticAddressWithdrawal) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressWithdrawal.ProtoReflect.Descriptor instead. func (*StaticAddressWithdrawal) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{71} + return file_client_proto_rawDescGZIP(), []int{75} } func (x *StaticAddressWithdrawal) GetTxId() string { @@ -5935,7 +6141,7 @@ type StaticAddressLoopInSwap struct { func (x *StaticAddressLoopInSwap) Reset() { *x = StaticAddressLoopInSwap{} - mi := &file_client_proto_msgTypes[72] + mi := &file_client_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5947,7 +6153,7 @@ func (x *StaticAddressLoopInSwap) String() string { func (*StaticAddressLoopInSwap) ProtoMessage() {} func (x *StaticAddressLoopInSwap) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[72] + mi := &file_client_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5960,7 +6166,7 @@ func (x *StaticAddressLoopInSwap) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressLoopInSwap.ProtoReflect.Descriptor instead. func (*StaticAddressLoopInSwap) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{72} + return file_client_proto_rawDescGZIP(), []int{76} } func (x *StaticAddressLoopInSwap) GetSwapHash() []byte { @@ -6093,7 +6299,7 @@ type StaticAddressLoopInRequest struct { func (x *StaticAddressLoopInRequest) Reset() { *x = StaticAddressLoopInRequest{} - mi := &file_client_proto_msgTypes[73] + mi := &file_client_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6105,7 +6311,7 @@ func (x *StaticAddressLoopInRequest) String() string { func (*StaticAddressLoopInRequest) ProtoMessage() {} func (x *StaticAddressLoopInRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[73] + mi := &file_client_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6118,7 +6324,7 @@ func (x *StaticAddressLoopInRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressLoopInRequest.ProtoReflect.Descriptor instead. func (*StaticAddressLoopInRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{73} + return file_client_proto_rawDescGZIP(), []int{77} } func (x *StaticAddressLoopInRequest) GetOutpoints() []string { @@ -6238,7 +6444,7 @@ type StaticAddressLoopInResponse struct { func (x *StaticAddressLoopInResponse) Reset() { *x = StaticAddressLoopInResponse{} - mi := &file_client_proto_msgTypes[74] + mi := &file_client_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6250,7 +6456,7 @@ func (x *StaticAddressLoopInResponse) String() string { func (*StaticAddressLoopInResponse) ProtoMessage() {} func (x *StaticAddressLoopInResponse) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[74] + mi := &file_client_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6263,7 +6469,7 @@ func (x *StaticAddressLoopInResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use StaticAddressLoopInResponse.ProtoReflect.Descriptor instead. func (*StaticAddressLoopInResponse) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{74} + return file_client_proto_rawDescGZIP(), []int{78} } func (x *StaticAddressLoopInResponse) GetSwapHash() []byte { @@ -6392,7 +6598,7 @@ type AssetLoopOutRequest struct { func (x *AssetLoopOutRequest) Reset() { *x = AssetLoopOutRequest{} - mi := &file_client_proto_msgTypes[75] + mi := &file_client_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6404,7 +6610,7 @@ func (x *AssetLoopOutRequest) String() string { func (*AssetLoopOutRequest) ProtoMessage() {} func (x *AssetLoopOutRequest) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[75] + mi := &file_client_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6417,7 +6623,7 @@ func (x *AssetLoopOutRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetLoopOutRequest.ProtoReflect.Descriptor instead. func (*AssetLoopOutRequest) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{75} + return file_client_proto_rawDescGZIP(), []int{79} } func (x *AssetLoopOutRequest) GetAssetId() []byte { @@ -6472,7 +6678,7 @@ type AssetRfqInfo struct { func (x *AssetRfqInfo) Reset() { *x = AssetRfqInfo{} - mi := &file_client_proto_msgTypes[76] + mi := &file_client_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6484,7 +6690,7 @@ func (x *AssetRfqInfo) String() string { func (*AssetRfqInfo) ProtoMessage() {} func (x *AssetRfqInfo) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[76] + mi := &file_client_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6497,7 +6703,7 @@ func (x *AssetRfqInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetRfqInfo.ProtoReflect.Descriptor instead. func (*AssetRfqInfo) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{76} + return file_client_proto_rawDescGZIP(), []int{80} } func (x *AssetRfqInfo) GetPrepayRfqId() []byte { @@ -6586,7 +6792,7 @@ type FixedPoint struct { func (x *FixedPoint) Reset() { *x = FixedPoint{} - mi := &file_client_proto_msgTypes[77] + mi := &file_client_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6598,7 +6804,7 @@ func (x *FixedPoint) String() string { func (*FixedPoint) ProtoMessage() {} func (x *FixedPoint) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[77] + mi := &file_client_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6611,7 +6817,7 @@ func (x *FixedPoint) ProtoReflect() protoreflect.Message { // Deprecated: Use FixedPoint.ProtoReflect.Descriptor instead. func (*FixedPoint) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{77} + return file_client_proto_rawDescGZIP(), []int{81} } func (x *FixedPoint) GetCoefficient() string { @@ -6642,7 +6848,7 @@ type AssetLoopOutInfo struct { func (x *AssetLoopOutInfo) Reset() { *x = AssetLoopOutInfo{} - mi := &file_client_proto_msgTypes[78] + mi := &file_client_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6654,7 +6860,7 @@ func (x *AssetLoopOutInfo) String() string { func (*AssetLoopOutInfo) ProtoMessage() {} func (x *AssetLoopOutInfo) ProtoReflect() protoreflect.Message { - mi := &file_client_proto_msgTypes[78] + mi := &file_client_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6667,7 +6873,7 @@ func (x *AssetLoopOutInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use AssetLoopOutInfo.ProtoReflect.Descriptor instead. func (*AssetLoopOutInfo) Descriptor() ([]byte, []int) { - return file_client_proto_rawDescGZIP(), []int{78} + return file_client_proto_rawDescGZIP(), []int{82} } func (x *AssetLoopOutInfo) GetAssetId() string { @@ -6973,7 +7179,19 @@ const file_client_proto_rawDesc = "" + "\x06amount\x18\x03 \x01(\x04R\x06amount\x12\x13\n" + "\x05tx_id\x18\x04 \x01(\tR\x04txId\x12\x12\n" + "\x04vout\x18\x05 \x01(\rR\x04vout\x12\x16\n" + - "\x06expiry\x18\x06 \x01(\rR\x06expiry\"\xae\x01\n" + + "\x06expiry\x18\x06 \x01(\rR\x06expiry\"k\n" + + "\x19ReservationRequestRequest\x12\x10\n" + + "\x03amt\x18\x01 \x01(\x04R\x03amt\x12\x16\n" + + "\x06expiry\x18\x02 \x01(\rR\x06expiry\x12$\n" + + "\x0emax_prepay_amt\x18\x03 \x01(\x04R\fmaxPrepayAmt\"Z\n" + + "\x1aReservationRequestResponse\x12<\n" + + "\vreservation\x18\x01 \x01(\v2\x1a.looprpc.ClientReservationR\vreservation\"C\n" + + "\x17ReservationQuoteRequest\x12\x10\n" + + "\x03amt\x18\x01 \x01(\x04R\x03amt\x12\x16\n" + + "\x06expiry\x18\x02 \x01(\rR\x06expiry\"9\n" + + "\x18ReservationQuoteResponse\x12\x1d\n" + + "\n" + + "prepay_amt\x18\x01 \x01(\x04R\tprepayAmt\"\xae\x01\n" + "\x11InstantOutRequest\x12'\n" + "\x0freservation_ids\x18\x01 \x03(\fR\x0ereservationIds\x12*\n" + "\x11outgoing_chan_set\x18\x02 \x03(\x04R\x0foutgoingChanSet\x12\x1b\n" + @@ -7213,7 +7431,7 @@ const file_client_proto_rawDesc = "" + "\x1eSUCCEEDED_TRANSITIONING_FAILED\x10\t\x12\x13\n" + "\x0fUNLOCK_DEPOSITS\x10\n" + "\x12\x1e\n" + - "\x1aFAILED_STATIC_ADDRESS_SWAP\x10\v2\xca\x14\n" + + "\x1aFAILED_STATIC_ADDRESS_SWAP\x10\v2\x82\x16\n" + "\n" + "SwapClient\x129\n" + "\aLoopOut\x12\x17.looprpc.LoopOutRequest\x1a\x15.looprpc.SwapResponse\x127\n" + @@ -7237,7 +7455,9 @@ const file_client_proto_rawDesc = "" + "\x12GetLiquidityParams\x12\".looprpc.GetLiquidityParamsRequest\x1a\x1c.looprpc.LiquidityParameters\x12]\n" + "\x12SetLiquidityParams\x12\".looprpc.SetLiquidityParamsRequest\x1a#.looprpc.SetLiquidityParamsResponse\x12K\n" + "\fSuggestSwaps\x12\x1c.looprpc.SuggestSwapsRequest\x1a\x1d.looprpc.SuggestSwapsResponse\x12W\n" + - "\x10ListReservations\x12 .looprpc.ListReservationsRequest\x1a!.looprpc.ListReservationsResponse\x12E\n" + + "\x10ListReservations\x12 .looprpc.ListReservationsRequest\x1a!.looprpc.ListReservationsResponse\x12]\n" + + "\x12ReservationRequest\x12\".looprpc.ReservationRequestRequest\x1a#.looprpc.ReservationRequestResponse\x12W\n" + + "\x10ReservationQuote\x12 .looprpc.ReservationQuoteRequest\x1a!.looprpc.ReservationQuoteResponse\x12E\n" + "\n" + "InstantOut\x12\x1a.looprpc.InstantOutRequest\x1a\x1b.looprpc.InstantOutResponse\x12T\n" + "\x0fInstantOutQuote\x12\x1f.looprpc.InstantOutQuoteRequest\x1a .looprpc.InstantOutQuoteResponse\x12T\n" + @@ -7265,7 +7485,7 @@ func file_client_proto_rawDescGZIP() []byte { } var file_client_proto_enumTypes = make([]protoimpl.EnumInfo, 10) -var file_client_proto_msgTypes = make([]protoimpl.MessageInfo, 80) +var file_client_proto_msgTypes = make([]protoimpl.MessageInfo, 84) var file_client_proto_goTypes = []any{ (AddressType)(0), // 0: looprpc.AddressType (SwapType)(0), // 1: looprpc.SwapType @@ -7325,69 +7545,73 @@ var file_client_proto_goTypes = []any{ (*ListReservationsRequest)(nil), // 55: looprpc.ListReservationsRequest (*ListReservationsResponse)(nil), // 56: looprpc.ListReservationsResponse (*ClientReservation)(nil), // 57: looprpc.ClientReservation - (*InstantOutRequest)(nil), // 58: looprpc.InstantOutRequest - (*InstantOutResponse)(nil), // 59: looprpc.InstantOutResponse - (*InstantOutQuoteRequest)(nil), // 60: looprpc.InstantOutQuoteRequest - (*InstantOutQuoteResponse)(nil), // 61: looprpc.InstantOutQuoteResponse - (*ListInstantOutsRequest)(nil), // 62: looprpc.ListInstantOutsRequest - (*ListInstantOutsResponse)(nil), // 63: looprpc.ListInstantOutsResponse - (*InstantOut)(nil), // 64: looprpc.InstantOut - (*NewStaticAddressRequest)(nil), // 65: looprpc.NewStaticAddressRequest - (*NewStaticAddressResponse)(nil), // 66: looprpc.NewStaticAddressResponse - (*ListUnspentDepositsRequest)(nil), // 67: looprpc.ListUnspentDepositsRequest - (*ListUnspentDepositsResponse)(nil), // 68: looprpc.ListUnspentDepositsResponse - (*Utxo)(nil), // 69: looprpc.Utxo - (*WithdrawDepositsRequest)(nil), // 70: looprpc.WithdrawDepositsRequest - (*WithdrawDepositsResponse)(nil), // 71: looprpc.WithdrawDepositsResponse - (*ListStaticAddressDepositsRequest)(nil), // 72: looprpc.ListStaticAddressDepositsRequest - (*ListStaticAddressDepositsResponse)(nil), // 73: looprpc.ListStaticAddressDepositsResponse - (*ListStaticAddressWithdrawalRequest)(nil), // 74: looprpc.ListStaticAddressWithdrawalRequest - (*ListStaticAddressWithdrawalResponse)(nil), // 75: looprpc.ListStaticAddressWithdrawalResponse - (*ListStaticAddressSwapsRequest)(nil), // 76: looprpc.ListStaticAddressSwapsRequest - (*ListStaticAddressSwapsResponse)(nil), // 77: looprpc.ListStaticAddressSwapsResponse - (*StaticAddressSummaryRequest)(nil), // 78: looprpc.StaticAddressSummaryRequest - (*StaticAddressSummaryResponse)(nil), // 79: looprpc.StaticAddressSummaryResponse - (*Deposit)(nil), // 80: looprpc.Deposit - (*StaticAddressWithdrawal)(nil), // 81: looprpc.StaticAddressWithdrawal - (*StaticAddressLoopInSwap)(nil), // 82: looprpc.StaticAddressLoopInSwap - (*StaticAddressLoopInRequest)(nil), // 83: looprpc.StaticAddressLoopInRequest - (*StaticAddressLoopInResponse)(nil), // 84: looprpc.StaticAddressLoopInResponse - (*AssetLoopOutRequest)(nil), // 85: looprpc.AssetLoopOutRequest - (*AssetRfqInfo)(nil), // 86: looprpc.AssetRfqInfo - (*FixedPoint)(nil), // 87: looprpc.FixedPoint - (*AssetLoopOutInfo)(nil), // 88: looprpc.AssetLoopOutInfo - nil, // 89: looprpc.LiquidityParameters.EasyAssetParamsEntry - (*lnrpc.OpenChannelRequest)(nil), // 90: lnrpc.OpenChannelRequest - (*swapserverrpc.RouteHint)(nil), // 91: looprpc.RouteHint - (*lnrpc.OutPoint)(nil), // 92: lnrpc.OutPoint + (*ReservationRequestRequest)(nil), // 58: looprpc.ReservationRequestRequest + (*ReservationRequestResponse)(nil), // 59: looprpc.ReservationRequestResponse + (*ReservationQuoteRequest)(nil), // 60: looprpc.ReservationQuoteRequest + (*ReservationQuoteResponse)(nil), // 61: looprpc.ReservationQuoteResponse + (*InstantOutRequest)(nil), // 62: looprpc.InstantOutRequest + (*InstantOutResponse)(nil), // 63: looprpc.InstantOutResponse + (*InstantOutQuoteRequest)(nil), // 64: looprpc.InstantOutQuoteRequest + (*InstantOutQuoteResponse)(nil), // 65: looprpc.InstantOutQuoteResponse + (*ListInstantOutsRequest)(nil), // 66: looprpc.ListInstantOutsRequest + (*ListInstantOutsResponse)(nil), // 67: looprpc.ListInstantOutsResponse + (*InstantOut)(nil), // 68: looprpc.InstantOut + (*NewStaticAddressRequest)(nil), // 69: looprpc.NewStaticAddressRequest + (*NewStaticAddressResponse)(nil), // 70: looprpc.NewStaticAddressResponse + (*ListUnspentDepositsRequest)(nil), // 71: looprpc.ListUnspentDepositsRequest + (*ListUnspentDepositsResponse)(nil), // 72: looprpc.ListUnspentDepositsResponse + (*Utxo)(nil), // 73: looprpc.Utxo + (*WithdrawDepositsRequest)(nil), // 74: looprpc.WithdrawDepositsRequest + (*WithdrawDepositsResponse)(nil), // 75: looprpc.WithdrawDepositsResponse + (*ListStaticAddressDepositsRequest)(nil), // 76: looprpc.ListStaticAddressDepositsRequest + (*ListStaticAddressDepositsResponse)(nil), // 77: looprpc.ListStaticAddressDepositsResponse + (*ListStaticAddressWithdrawalRequest)(nil), // 78: looprpc.ListStaticAddressWithdrawalRequest + (*ListStaticAddressWithdrawalResponse)(nil), // 79: looprpc.ListStaticAddressWithdrawalResponse + (*ListStaticAddressSwapsRequest)(nil), // 80: looprpc.ListStaticAddressSwapsRequest + (*ListStaticAddressSwapsResponse)(nil), // 81: looprpc.ListStaticAddressSwapsResponse + (*StaticAddressSummaryRequest)(nil), // 82: looprpc.StaticAddressSummaryRequest + (*StaticAddressSummaryResponse)(nil), // 83: looprpc.StaticAddressSummaryResponse + (*Deposit)(nil), // 84: looprpc.Deposit + (*StaticAddressWithdrawal)(nil), // 85: looprpc.StaticAddressWithdrawal + (*StaticAddressLoopInSwap)(nil), // 86: looprpc.StaticAddressLoopInSwap + (*StaticAddressLoopInRequest)(nil), // 87: looprpc.StaticAddressLoopInRequest + (*StaticAddressLoopInResponse)(nil), // 88: looprpc.StaticAddressLoopInResponse + (*AssetLoopOutRequest)(nil), // 89: looprpc.AssetLoopOutRequest + (*AssetRfqInfo)(nil), // 90: looprpc.AssetRfqInfo + (*FixedPoint)(nil), // 91: looprpc.FixedPoint + (*AssetLoopOutInfo)(nil), // 92: looprpc.AssetLoopOutInfo + nil, // 93: looprpc.LiquidityParameters.EasyAssetParamsEntry + (*lnrpc.OpenChannelRequest)(nil), // 94: lnrpc.OpenChannelRequest + (*swapserverrpc.RouteHint)(nil), // 95: looprpc.RouteHint + (*lnrpc.OutPoint)(nil), // 96: lnrpc.OutPoint } var file_client_proto_depIdxs = []int32{ - 90, // 0: looprpc.StaticOpenChannelRequest.open_channel_request:type_name -> lnrpc.OpenChannelRequest + 94, // 0: looprpc.StaticOpenChannelRequest.open_channel_request:type_name -> lnrpc.OpenChannelRequest 0, // 1: looprpc.LoopOutRequest.account_addr_type:type_name -> looprpc.AddressType - 85, // 2: looprpc.LoopOutRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest - 86, // 3: looprpc.LoopOutRequest.asset_rfq_info:type_name -> looprpc.AssetRfqInfo - 91, // 4: looprpc.LoopInRequest.route_hints:type_name -> looprpc.RouteHint + 89, // 2: looprpc.LoopOutRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest + 90, // 3: looprpc.LoopOutRequest.asset_rfq_info:type_name -> looprpc.AssetRfqInfo + 95, // 4: looprpc.LoopInRequest.route_hints:type_name -> looprpc.RouteHint 1, // 5: looprpc.SwapStatus.type:type_name -> looprpc.SwapType 2, // 6: looprpc.SwapStatus.state:type_name -> looprpc.SwapState 8, // 7: looprpc.SwapStatus.static_loop_in_state:type_name -> looprpc.StaticAddressLoopInSwapState 3, // 8: looprpc.SwapStatus.failure_reason:type_name -> looprpc.FailureReason - 88, // 9: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo + 92, // 9: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo 20, // 10: looprpc.ListSwapsRequest.list_swap_filter:type_name -> looprpc.ListSwapsFilter 9, // 11: looprpc.ListSwapsFilter.swap_type:type_name -> looprpc.ListSwapsFilter.SwapTypeFilter 18, // 12: looprpc.ListSwapsResponse.swaps:type_name -> looprpc.SwapStatus 24, // 13: looprpc.SweepHtlcResponse.not_requested:type_name -> looprpc.PublishNotRequested 25, // 14: looprpc.SweepHtlcResponse.published:type_name -> looprpc.PublishSucceeded 26, // 15: looprpc.SweepHtlcResponse.failed:type_name -> looprpc.PublishFailed - 91, // 16: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint - 85, // 17: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest - 86, // 18: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo - 91, // 19: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint + 95, // 16: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint + 89, // 17: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest + 90, // 18: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo + 95, // 19: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint 40, // 20: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token 41, // 21: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats 41, // 22: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats 47, // 23: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule 0, // 24: looprpc.LiquidityParameters.account_addr_type:type_name -> looprpc.AddressType - 89, // 25: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry + 93, // 25: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry 4, // 26: looprpc.LiquidityParameters.loop_in_source:type_name -> looprpc.LoopInSource 1, // 27: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType 5, // 28: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType @@ -7395,96 +7619,101 @@ var file_client_proto_depIdxs = []int32{ 6, // 30: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason 14, // 31: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest 15, // 32: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest - 83, // 33: looprpc.SuggestSwapsResponse.static_loop_in:type_name -> looprpc.StaticAddressLoopInRequest + 87, // 33: looprpc.SuggestSwapsResponse.static_loop_in:type_name -> looprpc.StaticAddressLoopInRequest 51, // 34: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified 57, // 35: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation - 64, // 36: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut - 69, // 37: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo - 92, // 38: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint - 7, // 39: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState - 80, // 40: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit - 81, // 41: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal - 82, // 42: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap - 7, // 43: looprpc.Deposit.state:type_name -> looprpc.DepositState - 80, // 44: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit - 8, // 45: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState - 80, // 46: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit - 91, // 47: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint - 80, // 48: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit - 87, // 49: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint - 87, // 50: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint - 46, // 51: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams - 14, // 52: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest - 15, // 53: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest - 17, // 54: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest - 19, // 55: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest - 22, // 56: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest - 27, // 57: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest - 53, // 58: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest - 28, // 59: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest - 31, // 60: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest - 28, // 61: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest - 31, // 62: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest - 34, // 63: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest - 36, // 64: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest - 36, // 65: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest - 38, // 66: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest - 42, // 67: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest - 12, // 68: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest - 44, // 69: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest - 48, // 70: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest - 50, // 71: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest - 55, // 72: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest - 58, // 73: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest - 60, // 74: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest - 62, // 75: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest - 65, // 76: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest - 67, // 77: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest - 70, // 78: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest - 72, // 79: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest - 74, // 80: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest - 76, // 81: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest - 78, // 82: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest - 83, // 83: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest - 10, // 84: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest - 16, // 85: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse - 16, // 86: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse - 18, // 87: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus - 21, // 88: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse - 23, // 89: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse - 18, // 90: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus - 54, // 91: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse - 30, // 92: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse - 33, // 93: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse - 29, // 94: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse - 32, // 95: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse - 35, // 96: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse - 37, // 97: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse - 37, // 98: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse - 39, // 99: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse - 43, // 100: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse - 13, // 101: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse - 45, // 102: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters - 49, // 103: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse - 52, // 104: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse - 56, // 105: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse - 59, // 106: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse - 61, // 107: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse - 63, // 108: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse - 66, // 109: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse - 68, // 110: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse - 71, // 111: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse - 73, // 112: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse - 75, // 113: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse - 77, // 114: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse - 79, // 115: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse - 84, // 116: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse - 11, // 117: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse - 85, // [85:118] is the sub-list for method output_type - 52, // [52:85] is the sub-list for method input_type - 52, // [52:52] is the sub-list for extension type_name - 52, // [52:52] is the sub-list for extension extendee - 0, // [0:52] is the sub-list for field type_name + 57, // 36: looprpc.ReservationRequestResponse.reservation:type_name -> looprpc.ClientReservation + 68, // 37: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut + 73, // 38: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo + 96, // 39: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint + 7, // 40: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState + 84, // 41: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit + 85, // 42: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal + 86, // 43: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap + 7, // 44: looprpc.Deposit.state:type_name -> looprpc.DepositState + 84, // 45: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit + 8, // 46: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState + 84, // 47: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit + 95, // 48: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint + 84, // 49: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit + 91, // 50: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint + 91, // 51: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint + 46, // 52: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams + 14, // 53: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest + 15, // 54: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest + 17, // 55: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest + 19, // 56: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest + 22, // 57: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest + 27, // 58: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest + 53, // 59: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest + 28, // 60: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest + 31, // 61: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest + 28, // 62: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest + 31, // 63: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest + 34, // 64: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest + 36, // 65: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest + 36, // 66: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest + 38, // 67: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest + 42, // 68: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest + 12, // 69: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest + 44, // 70: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest + 48, // 71: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest + 50, // 72: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest + 55, // 73: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest + 58, // 74: looprpc.SwapClient.ReservationRequest:input_type -> looprpc.ReservationRequestRequest + 60, // 75: looprpc.SwapClient.ReservationQuote:input_type -> looprpc.ReservationQuoteRequest + 62, // 76: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest + 64, // 77: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest + 66, // 78: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest + 69, // 79: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest + 71, // 80: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest + 74, // 81: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest + 76, // 82: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest + 78, // 83: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest + 80, // 84: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest + 82, // 85: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest + 87, // 86: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest + 10, // 87: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest + 16, // 88: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse + 16, // 89: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse + 18, // 90: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus + 21, // 91: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse + 23, // 92: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse + 18, // 93: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus + 54, // 94: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse + 30, // 95: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse + 33, // 96: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse + 29, // 97: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse + 32, // 98: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse + 35, // 99: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse + 37, // 100: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse + 37, // 101: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse + 39, // 102: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse + 43, // 103: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse + 13, // 104: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse + 45, // 105: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters + 49, // 106: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse + 52, // 107: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse + 56, // 108: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse + 59, // 109: looprpc.SwapClient.ReservationRequest:output_type -> looprpc.ReservationRequestResponse + 61, // 110: looprpc.SwapClient.ReservationQuote:output_type -> looprpc.ReservationQuoteResponse + 63, // 111: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse + 65, // 112: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse + 67, // 113: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse + 70, // 114: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse + 72, // 115: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse + 75, // 116: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse + 77, // 117: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse + 79, // 118: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse + 81, // 119: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse + 83, // 120: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse + 88, // 121: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse + 11, // 122: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse + 88, // [88:123] is the sub-list for method output_type + 53, // [53:88] is the sub-list for method input_type + 53, // [53:53] is the sub-list for extension type_name + 53, // [53:53] is the sub-list for extension extendee + 0, // [0:53] is the sub-list for field type_name } func init() { file_client_proto_init() } @@ -7506,7 +7735,7 @@ func file_client_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_client_proto_rawDesc), len(file_client_proto_rawDesc)), NumEnums: 10, - NumMessages: 80, + NumMessages: 84, NumExtensions: 0, NumServices: 1, }, diff --git a/looprpc/client.proto b/looprpc/client.proto index d36203008..3e80b136e 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -137,12 +137,25 @@ service SwapClient { */ rpc SuggestSwaps (SuggestSwapsRequest) returns (SuggestSwapsResponse); - /* loop: `listreservations` + /* loop: `reservations list` ListReservations returns a list of all reservations the server opened to us. */ rpc ListReservations (ListReservationsRequest) returns (ListReservationsResponse); + /* loop:`reservation request` +ReservationRequest requests a reservation from the server. +*/ + rpc ReservationRequest (ReservationRequestRequest) + returns (ReservationRequestResponse); + + /* loop:`reservation quote` + ReservationQuote returns a quote for a reservation with the provided + parameters. + */ + rpc ReservationQuote (ReservationQuoteRequest) + returns (ReservationQuoteResponse); + /* loop: `instantout` InstantOut initiates an instant out swap with the given parameters. */ @@ -1667,6 +1680,46 @@ message ClientReservation { uint32 expiry = 6; } +message ReservationRequestRequest { + /* + The amount to reserve in satoshis. + */ + uint64 amt = 1; + + /* + The relative expiry of the reservation in blocks. + */ + uint32 expiry = 2; + + /* + The maximum amt in satoshis we allow for the prepayment. + */ + uint64 max_prepay_amt = 3; +} + +message ReservationRequestResponse { + ClientReservation reservation = 1; +} + +message ReservationQuoteRequest { + /* + The amount to reserve in satoshis. + */ + uint64 amt = 1; + + /* + The relative expiry of the reservation in blocks. + */ + uint32 expiry = 2; +} + +message ReservationQuoteResponse { + /* + The prepay fee that will be charged for the reservation. + */ + uint64 prepay_amt = 1; +} + message InstantOutRequest { /* The reservations to use for the swap. diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index b75794320..ac902f363 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -130,7 +130,7 @@ }, "/v1/instantout/reservations": { "get": { - "summary": "loop: `listreservations`\nListReservations returns a list of all reservations the server opened to us.", + "summary": "loop: `reservations list`\nListReservations returns a list of all reservations the server opened to us.", "operationId": "SwapClient_ListReservations", "responses": { "200": { @@ -2621,6 +2621,24 @@ "type": "object", "description": "PublishSucceeded is returned by SweepHtlc if publishing was requested in\nSweepHtlcRequest and it succeeded." }, + "looprpcReservationQuoteResponse": { + "type": "object", + "properties": { + "prepay_amt": { + "type": "string", + "format": "uint64", + "description": "The prepay fee that will be charged for the reservation." + } + } + }, + "looprpcReservationRequestResponse": { + "type": "object", + "properties": { + "reservation": { + "$ref": "#/definitions/looprpcClientReservation" + } + } + }, "looprpcRouteHint": { "type": "object", "properties": { diff --git a/looprpc/client_grpc.pb.go b/looprpc/client_grpc.pb.go index b03cc9e87..c91d40715 100644 --- a/looprpc/client_grpc.pb.go +++ b/looprpc/client_grpc.pb.go @@ -99,9 +99,16 @@ type SwapClientClient interface { // Note that only loop out suggestions are currently supported. // [EXPERIMENTAL]: endpoint is subject to change. SuggestSwaps(ctx context.Context, in *SuggestSwapsRequest, opts ...grpc.CallOption) (*SuggestSwapsResponse, error) - // loop: `listreservations` + // loop: `reservations list` // ListReservations returns a list of all reservations the server opened to us. ListReservations(ctx context.Context, in *ListReservationsRequest, opts ...grpc.CallOption) (*ListReservationsResponse, error) + // loop:`reservation request` + // ReservationRequest requests a reservation from the server. + ReservationRequest(ctx context.Context, in *ReservationRequestRequest, opts ...grpc.CallOption) (*ReservationRequestResponse, error) + // loop:`reservation quote` + // ReservationQuote returns a quote for a reservation with the provided + // parameters. + ReservationQuote(ctx context.Context, in *ReservationQuoteRequest, opts ...grpc.CallOption) (*ReservationQuoteResponse, error) // loop: `instantout` // InstantOut initiates an instant out swap with the given parameters. InstantOut(ctx context.Context, in *InstantOutRequest, opts ...grpc.CallOption) (*InstantOutResponse, error) @@ -366,6 +373,24 @@ func (c *swapClientClient) ListReservations(ctx context.Context, in *ListReserva return out, nil } +func (c *swapClientClient) ReservationRequest(ctx context.Context, in *ReservationRequestRequest, opts ...grpc.CallOption) (*ReservationRequestResponse, error) { + out := new(ReservationRequestResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapClient/ReservationRequest", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *swapClientClient) ReservationQuote(ctx context.Context, in *ReservationQuoteRequest, opts ...grpc.CallOption) (*ReservationQuoteResponse, error) { + out := new(ReservationQuoteResponse) + err := c.cc.Invoke(ctx, "/looprpc.SwapClient/ReservationQuote", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *swapClientClient) InstantOut(ctx context.Context, in *InstantOutRequest, opts ...grpc.CallOption) (*InstantOutResponse, error) { out := new(InstantOutResponse) err := c.cc.Invoke(ctx, "/looprpc.SwapClient/InstantOut", in, out, opts...) @@ -559,9 +584,16 @@ type SwapClientServer interface { // Note that only loop out suggestions are currently supported. // [EXPERIMENTAL]: endpoint is subject to change. SuggestSwaps(context.Context, *SuggestSwapsRequest) (*SuggestSwapsResponse, error) - // loop: `listreservations` + // loop: `reservations list` // ListReservations returns a list of all reservations the server opened to us. ListReservations(context.Context, *ListReservationsRequest) (*ListReservationsResponse, error) + // loop:`reservation request` + // ReservationRequest requests a reservation from the server. + ReservationRequest(context.Context, *ReservationRequestRequest) (*ReservationRequestResponse, error) + // loop:`reservation quote` + // ReservationQuote returns a quote for a reservation with the provided + // parameters. + ReservationQuote(context.Context, *ReservationQuoteRequest) (*ReservationQuoteResponse, error) // loop: `instantout` // InstantOut initiates an instant out swap with the given parameters. InstantOut(context.Context, *InstantOutRequest) (*InstantOutResponse, error) @@ -674,6 +706,12 @@ func (UnimplementedSwapClientServer) SuggestSwaps(context.Context, *SuggestSwaps func (UnimplementedSwapClientServer) ListReservations(context.Context, *ListReservationsRequest) (*ListReservationsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListReservations not implemented") } +func (UnimplementedSwapClientServer) ReservationRequest(context.Context, *ReservationRequestRequest) (*ReservationRequestResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ReservationRequest not implemented") +} +func (UnimplementedSwapClientServer) ReservationQuote(context.Context, *ReservationQuoteRequest) (*ReservationQuoteResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ReservationQuote not implemented") +} func (UnimplementedSwapClientServer) InstantOut(context.Context, *InstantOutRequest) (*InstantOutResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method InstantOut not implemented") } @@ -1104,6 +1142,42 @@ func _SwapClient_ListReservations_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _SwapClient_ReservationRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReservationRequestRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).ReservationRequest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapClient/ReservationRequest", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).ReservationRequest(ctx, req.(*ReservationRequestRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwapClient_ReservationQuote_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ReservationQuoteRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwapClientServer).ReservationQuote(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/looprpc.SwapClient/ReservationQuote", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwapClientServer).ReservationQuote(ctx, req.(*ReservationQuoteRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _SwapClient_InstantOut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(InstantOutRequest) if err := dec(in); err != nil { @@ -1407,6 +1481,14 @@ var SwapClient_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListReservations", Handler: _SwapClient_ListReservations_Handler, }, + { + MethodName: "ReservationRequest", + Handler: _SwapClient_ReservationRequest_Handler, + }, + { + MethodName: "ReservationQuote", + Handler: _SwapClient_ReservationQuote_Handler, + }, { MethodName: "InstantOut", Handler: _SwapClient_InstantOut_Handler, diff --git a/looprpc/perms.go b/looprpc/perms.go index 9187920a7..4a438914f 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -181,6 +181,14 @@ var RequiredPermissions = map[string][]bakery.Op{ Entity: "loop", Action: "out", }}, + "/looprpc.SwapClient/ReservationRequest": {{ + Entity: "swap", + Action: "execute", + }}, + "/looprpc.SwapClient/ReservationQuote": {{ + Entity: "swap", + Action: "read", + }}, "/looprpc.SwapClient/InstantOut": {{ Entity: "swap", Action: "execute", diff --git a/looprpc/swapclient.pb.json.go b/looprpc/swapclient.pb.json.go index ef1297dc3..e6c8c019b 100644 --- a/looprpc/swapclient.pb.json.go +++ b/looprpc/swapclient.pb.json.go @@ -563,6 +563,56 @@ func RegisterSwapClientJSONCallbacks(registry map[string]func(ctx context.Contex callback(string(respBytes), nil) } + registry["looprpc.SwapClient.ReservationRequest"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ReservationRequestRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewSwapClientClient(conn) + resp, err := client.ReservationRequest(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + + registry["looprpc.SwapClient.ReservationQuote"] = func(ctx context.Context, + conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { + + req := &ReservationQuoteRequest{} + err := marshaler.Unmarshal([]byte(reqJSON), req) + if err != nil { + callback("", err) + return + } + + client := NewSwapClientClient(conn) + resp, err := client.ReservationQuote(ctx, req) + if err != nil { + callback("", err) + return + } + + respBytes, err := marshaler.Marshal(resp) + if err != nil { + callback("", err) + return + } + callback(string(respBytes), nil) + } + registry["looprpc.SwapClient.InstantOut"] = func(ctx context.Context, conn *grpc.ClientConn, reqJSON string, callback func(string, error)) { From b48fd9aa7c89f8941c95bd12d0deaa4f56f6c34b Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 3 Feb 2025 16:14:23 +0100 Subject: [PATCH 20/35] looprpc_server: add reservation calls --- loopd/swapclient_server.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index dd5e0f051..8bef9d1f1 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -1739,6 +1739,38 @@ func (s *swapClientServer) ListReservations(ctx context.Context, }, nil } +func (s *swapClientServer) ReservationRequest(ctx context.Context, + req *looprpc.ReservationRequestRequest) ( + *looprpc.ReservationRequestResponse, error) { + + reservation, err := s.reservationManager.RequestReservationFromServer( + ctx, btcutil.Amount(req.Amt), req.Expiry, + btcutil.Amount(req.MaxPrepayAmt), + ) + if err != nil { + return nil, err + } + + return &looprpc.ReservationRequestResponse{ + Reservation: toClientReservation(reservation), + }, nil +} +func (s *swapClientServer) ReservationQuote(ctx context.Context, + req *looprpc.ReservationQuoteRequest) ( + *looprpc.ReservationQuoteResponse, error) { + + quote, err := s.reservationManager.QuoteReservation( + ctx, btcutil.Amount(req.Amt), req.Expiry, + ) + if err != nil { + return nil, err + } + + return &looprpc.ReservationQuoteResponse{ + PrepayAmt: uint64(quote), + }, nil +} + // InstantOut initiates an instant out swap. func (s *swapClientServer) InstantOut(ctx context.Context, req *looprpc.InstantOutRequest) (*looprpc.InstantOutResponse, From 56be61c31d97e8dab88dcff059f03f5dcf4cb281 Mon Sep 17 00:00:00 2001 From: sputn1ck Date: Mon, 3 Feb 2025 16:14:39 +0100 Subject: [PATCH 21/35] cmd: add new reservation clis --- cmd/loop/main.go | 2 + cmd/loop/reservations.go | 83 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 127634d2f..dc57ddc12 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -41,6 +41,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) diff --git a/cmd/loop/reservations.go b/cmd/loop/reservations.go index 122eda12f..196610f71 100644 --- a/cmd/loop/reservations.go +++ b/cmd/loop/reservations.go @@ -2,13 +2,26 @@ package main import ( "context" + "errors" + "fmt" "github.com/lightninglabs/loop/looprpc" "github.com/urfave/cli/v3" ) -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", Aliases: []string{"r"}, Usage: "manage reservations", @@ -20,6 +33,7 @@ var reservationsCommands = &cli.Command{ `, Commands: []*cli.Command{ listReservationsCommand, + newReservationCommand, }, } @@ -34,8 +48,75 @@ var ( `, Action: listReservations, } + + newReservationCommand = &cli.Command{ + Name: "new", + Aliases: []string{"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 context.Context, cmd *cli.Command) error { + client, cleanup, err := getClient(cmd) + if err != nil { + return err + } + defer cleanup() + + rpcCtx, cancel := context.WithTimeout(ctx, defaultRpcTimeout) + defer cancel() + + if !cmd.IsSet(reservationAmountFlag.Name) { + return errors.New("amt flag missing") + } + + if !cmd.IsSet(reservationExpiryFlag.Name) { + return errors.New("expiry flag missing") + } + + quoteReq, err := client.ReservationQuote( + rpcCtx, &looprpc.ReservationQuoteRequest{ + Amt: cmd.Uint64(reservationAmountFlag.Name), + Expiry: uint32(cmd.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( + rpcCtx, &looprpc.ReservationRequestRequest{ + Amt: cmd.Uint64(reservationAmountFlag.Name), + Expiry: uint32(cmd.Uint(reservationExpiryFlag.Name)), + MaxPrepayAmt: quoteReq.PrepayAmt, + }, + ) + if err != nil { + return err + } + + printRespJSON(reservationRes) + return nil +} + func listReservations(ctx context.Context, cmd *cli.Command) error { client, cleanup, err := getClient(cmd) if err != nil { From e4c4e116274ff393b38493903a19d2650e002f37 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 15:22:05 +0200 Subject: [PATCH 22/35] loopd: nil-guard reservation/instant-out RPC handlers When loopd is started without --experimental the swap client server's reservationManager and instantOutManager are nil. ListReservations already returns codes.Unimplemented in that case; the rest of the instant-out / reservation RPC family didn't, and would dereference a nil pointer. Affected handlers (all of which now return the same Unimplemented status): - ReservationRequest (new in PR #883) - ReservationQuote (new in PR #883) - InstantOut - InstantOutQuote - ListInstantOuts Without this fix an authenticated caller can crash the daemon by invoking any of these RPCs against a non-experimental loopd. With default localhost binding the attack surface is small, but loop is also commonly fronted by lit / LSP wrappers that expose RPCs to other internal services, so a single packet is enough for a remote DoS. --- loopd/swapclient_server.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index 8bef9d1f1..81d72f2ae 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -1743,6 +1743,11 @@ func (s *swapClientServer) ReservationRequest(ctx context.Context, req *looprpc.ReservationRequestRequest) ( *looprpc.ReservationRequestResponse, error) { + if s.reservationManager == nil { + return nil, status.Error(codes.Unimplemented, + "Restart loop with --experimental") + } + reservation, err := s.reservationManager.RequestReservationFromServer( ctx, btcutil.Amount(req.Amt), req.Expiry, btcutil.Amount(req.MaxPrepayAmt), @@ -1755,10 +1760,16 @@ func (s *swapClientServer) ReservationRequest(ctx context.Context, Reservation: toClientReservation(reservation), }, nil } + func (s *swapClientServer) ReservationQuote(ctx context.Context, req *looprpc.ReservationQuoteRequest) ( *looprpc.ReservationQuoteResponse, error) { + if s.reservationManager == nil { + return nil, status.Error(codes.Unimplemented, + "Restart loop with --experimental") + } + quote, err := s.reservationManager.QuoteReservation( ctx, btcutil.Amount(req.Amt), req.Expiry, ) From 508b9dfdc1e95f2dd7f31fb7cf516a8f1daa0f5d Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 15:28:26 +0200 Subject: [PATCH 23/35] multi: require Loop Out permission for Instant Out The InstantOut RPC accepts a caller-controlled dest_addr that becomes the output of the cooperative sweepless sweep (and of the htlc success sweep on the fallback path), so it is a fund-moving operation equivalent to LoopOut. Until now it required only swap:execute, while LoopOut requires both swap:execute and loop:out. A macaroon scoped to swap:execute -- intended for, say, an autoloop scheduler or a quote poller -- could therefore drain reservation balances to an attacker address. ReservationRequest is analogous on the inbound side: it triggers an outgoing LN prepayment, so it also belongs behind loop:out. We also harden the address handling in instantout.Manager.NewInstantOut to match validateLoopOutRequest: - sweepAddr.IsForNet(m.cfg.Network) is now enforced. btcutil .DecodeAddress is more permissive than IsForNet for some formats (notably anything that happens to share a network prefix); without the explicit network check cross-chain copy-paste mistakes parse silently and then sign over an unspendable output. - The address must be one of the formats Loop normally accepts: P2TR / P2WSH / P2WPKH / P2SH / P2PKH. Anything else (e.g. a future address type that the user's wallet would otherwise interpret differently) is rejected up front rather than failing later in the signing path. InstantOutQuote and ReservationQuote stay on swap:read since they are read-only. --- instantout/manager.go | 18 ++++++++++++++++++ looprpc/perms.go | 3 +++ 2 files changed, 21 insertions(+) diff --git a/instantout/manager.go b/instantout/manager.go index bebd8f824..e96791e38 100644 --- a/instantout/manager.go +++ b/instantout/manager.go @@ -156,6 +156,24 @@ func (m *Manager) NewInstantOut(ctx context.Context, if err != nil { return nil, err } + + if !sweepAddr.IsForNet(m.cfg.Network) { + return nil, fmt.Errorf("sweep address %s is not "+ + "valid for network %s", sweepAddress, + m.cfg.Network.Name) + } + + switch sweepAddr.(type) { + case *btcutil.AddressTaproot, + *btcutil.AddressWitnessScriptHash, + *btcutil.AddressWitnessPubKeyHash, + *btcutil.AddressScriptHash, + *btcutil.AddressPubKeyHash: + + default: + return nil, fmt.Errorf("unsupported sweep address "+ + "type %T", sweepAddr) + } } m.Lock() diff --git a/looprpc/perms.go b/looprpc/perms.go index 4a438914f..eb7936872 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -184,6 +184,9 @@ var RequiredPermissions = map[string][]bakery.Op{ "/looprpc.SwapClient/ReservationRequest": {{ Entity: "swap", Action: "execute", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/ReservationQuote": {{ Entity: "swap", From eb58fcaa0342a28632ae09f6f8c504861add06d2 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 15:31:36 +0200 Subject: [PATCH 24/35] instantout/reservation: avoid uint32 underflow in expiry-bounds check InitFromClientRequestAction validates that the server-returned absolute expiry is within +/- expiryDelta of expectedExpiry = relativeExpiry + heightHint. Both sides were uint32, so when expectedExpiry < expiryDelta (low regtest heights, fresh deployments, anything with heightHint = 0 like the existing test fixtures) expectedExpiry - expiryDelta wrapped to ~2^32. The lower bound check then trivially admitted any reasonable response, and the client would accept e.g. Expiry = 0 from the server, immediately past the reservation's own deadline -- meaning the server can sweep via the expiry script path while the client still believes it owns the reservation slot. Promote the comparison to int64 so the arithmetic is sign-honest. This is the smallest patch that closes the underflow; a follow-up should also add an absolute floor (e.g. Expiry >= heightHint + minSafeExpiry) so the server cannot return a near-deadline reservation even within the delta. --- instantout/reservation/actions.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/instantout/reservation/actions.go b/instantout/reservation/actions.go index 8a805e4d4..0fd09b6df 100644 --- a/instantout/reservation/actions.go +++ b/instantout/reservation/actions.go @@ -76,9 +76,12 @@ func (f *FSM) InitFromClientRequestAction(ctx context.Context, expectedExpiry := reservationRequest.relativeExpiry + reservationRequest.heightHint - // Check that the expiry is in the delta. - if requestResponse.Expiry < expectedExpiry-expiryDelta || - requestResponse.Expiry > expectedExpiry+expiryDelta { + // Check that the expiry is in the delta. Compare as int64 so the + // lower bound stays meaningful when expectedExpiry < expiryDelta + // (which would otherwise underflow the uint32 and accept any + // response below the upper bound). + if int64(requestResponse.Expiry) < int64(expectedExpiry)-int64(expiryDelta) || + int64(requestResponse.Expiry) > int64(expectedExpiry)+int64(expiryDelta) { return f.HandleError( fmt.Errorf("unexpected expiry height: %v, expected %v", From 800dae07501a2db4a012f3239de3a8b314d25f20 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 15:33:37 +0200 Subject: [PATCH 25/35] instantout/reservation: honor context when queueing request RequestReservationFromServer dispatched the OnClientInitialized event to the manager's Run loop via a bare 'm.reqChan <- ...' send. reqChan is an unbuffered channel; if Run had already returned (e.g. because the block epoch subscription errored, or the manager is shutting down) the send would block forever, holding the gRPC handler goroutine and the caller's connection open until something external killed it. Wrap the send in a select that also watches the caller's context. A cancelled caller context now returns ctx.Err() instead of hanging. Note: this still does not detect "Run exited cleanly while reqChan was empty" -- doing that requires exposing Run's runCtx (or a quit channel) on the Manager struct. That refactor is left for a follow-up; the caller-side cancel path above is enough to keep RPC handlers from leaking when their grpc deadline fires. --- instantout/reservation/manager.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 42d163fb9..02c3fda44 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -263,11 +263,18 @@ func (m *Manager) RequestReservationFromServer(ctx context.Context, } reservationFSM := NewFSM(m.cfg, ProtocolVersionClientInitiated) - // Send the event to the main loop. - m.reqChan <- &FSMSendEventReq{ + // Send the event to the main loop. reqChan is unbuffered so the + // raw send blocks until Run picks it up; if Run has already exited + // or the caller has cancelled, fall through with an error instead + // of hanging the RPC indefinitely. + select { + case m.reqChan <- &FSMSendEventReq{ fsm: reservationFSM, event: OnClientInitialized, eventCtx: req, + }: + case <-ctx.Done(): + return nil, ctx.Err() } // We'll now wait for the reservation to be in the state where we are From 3c495a2a5c0323a6d9485ec6a320f18d4369dd47 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 16:04:23 +0200 Subject: [PATCH 26/35] cmd/loop: require explicit 'y' confirmation on reservation new The reservation new command printed the prepay cost and asked the user to confirm with 'y/n'. The implementation read the answer with fmt.Scanln(&answer) and treated only the literal 'n' as a 'no'. The return value was discarded, so: - On EOF / closed stdin (CI pipelines, automated wrappers, terminal disconnect) Scanln returned an error and answer remained the empty string, which is not 'n', so the command proceeded and paid the LN prepayment with no user confirmation. - The case-sensitive 'n' check also accepted 'N', 'no', 'yes', 'Y', or any other string as a 'yes'. Match the convention used by the rest of the loop CLI: only continue when the user typed exactly 'y' (or 'Y'), and treat any read error as 'no'. --- cmd/loop/reservations.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmd/loop/reservations.go b/cmd/loop/reservations.go index 196610f71..12563ac30 100644 --- a/cmd/loop/reservations.go +++ b/cmd/loop/reservations.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/lightninglabs/loop/looprpc" "github.com/urfave/cli/v3" @@ -97,8 +98,9 @@ func newReservation(ctx context.Context, cmd *cli.Command) error { fmt.Printf("CONTINUE RESERVATION? (y/n): ") var answer string - fmt.Scanln(&answer) - if answer == "n" { + if _, err := fmt.Scanln(&answer); err != nil || + !strings.EqualFold(answer, "y") { + return nil } From 402b431c80ecb5b34449651b0758698e90b3ae6f Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 16:10:20 +0200 Subject: [PATCH 27/35] instantout/reservation: persist client reservations as Init InitFromClientRequestAction wrote the new reservation row via Store.CreateReservation while reservation.State was still the zero value (fsm.EmptyState) returned by NewReservation. The GetClientInitiatedReservationStates() state map has no OnRecover transition on EmptyState. If the daemon crashed (or the context was cancelled) any time after CreateReservation returned, the row was permanently stuck: on restart RecoverReservations rebuilt the FSM at state "", SendEvent(OnRecover) returned "event not allowed", and the goroutine just logged and gave up. The HD key index was wasted; the server-side reservation was left orphan. Set reservation.State = Init before persisting. The Init state already has OnRecover: Failed, so a crashed-mid-Init reservation now recovers cleanly into Failed on the next start. updateReservation's existing skip-list keeps the immediately-following SendPrepaymentPayment transition working as before (it skips writes while in Init). A follow-up should also notify the server to cancel orphaned reservations from Failed.OnRecover; that requires plumbing a cancel-RPC into the client-initiated state map. --- instantout/reservation/actions.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/instantout/reservation/actions.go b/instantout/reservation/actions.go index 0fd09b6df..7758d6b8f 100644 --- a/instantout/reservation/actions.go +++ b/instantout/reservation/actions.go @@ -117,6 +117,13 @@ func (f *FSM) InitFromClientRequestAction(ctx context.Context, return f.HandleError(err) } reservation.PrepayInvoice = requestResponse.Invoice + + // Persist the row with state = Init so a crash before the next + // state transition leaves a recoverable row. Without this, NewReservation + // produces State = fsm.EmptyState (zero value), which has no OnRecover + // transition in the client-initiated state map, and recovery would + // silently leave the row stuck forever. + reservation.State = Init f.reservation = reservation // Create the reservation in the database. From b988ea8788427f8852e0ba805ed53ae6be7a798c Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 16:14:37 +0200 Subject: [PATCH 28/35] loopdb: lowercase protocol_version column in migration 14 Migration 000014_reservation_protocol_version used 'protocol_Version' (capital V) in the ADD COLUMN / DROP COLUMN statements. Postgres folds unquoted identifiers to lowercase and SQLite is case-insensitive on identifier comparison, so the running schema column is 'protocol_version' either way -- but the sqlc-generated Go (loopdb/sqlc/reservations.sql.go) also uses the lowercase form, so the file as written was both unusual and inconsistent with its own generated SQL. Use 'protocol_version' everywhere. No data migration is required; the column on disk is unchanged. Pure cosmetic / portability fix. --- .../migrations/000014_reservation_protocol_version.down.sql | 2 +- .../sqlc/migrations/000014_reservation_protocol_version.up.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/loopdb/sqlc/migrations/000014_reservation_protocol_version.down.sql b/loopdb/sqlc/migrations/000014_reservation_protocol_version.down.sql index d23af75ec..ca38f26ad 100644 --- a/loopdb/sqlc/migrations/000014_reservation_protocol_version.down.sql +++ b/loopdb/sqlc/migrations/000014_reservation_protocol_version.down.sql @@ -1,3 +1,3 @@ -- protocol_version is used to determine the version of the reservation protocol -- that was used to create the reservation. -ALTER TABLE reservations DROP COLUMN protocol_Version; +ALTER TABLE reservations DROP COLUMN protocol_version; diff --git a/loopdb/sqlc/migrations/000014_reservation_protocol_version.up.sql b/loopdb/sqlc/migrations/000014_reservation_protocol_version.up.sql index 29da580a0..464590abf 100644 --- a/loopdb/sqlc/migrations/000014_reservation_protocol_version.up.sql +++ b/loopdb/sqlc/migrations/000014_reservation_protocol_version.up.sql @@ -1,3 +1,3 @@ -- protocol_version is used to determine the version of the reservation protocol -- that was used to create the reservation. -ALTER TABLE reservations ADD COLUMN protocol_Version INTEGER NOT NULL DEFAULT 0; \ No newline at end of file +ALTER TABLE reservations ADD COLUMN protocol_version INTEGER NOT NULL DEFAULT 0; \ No newline at end of file From 17df6e86e6b0466d97a14dfa3b820ce6cd41f72b Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 16:19:46 +0200 Subject: [PATCH 29/35] instantout/reservation: extend RPC state wait timeout RequestReservationFromServer blocked for defaultWaitForStateTime (15s) waiting for the FSM to reach SendPrepaymentPayment. Reaching that state requires, in order: - Wallet.DeriveNextKey (local lnd round-trip) - server's RequestReservation gRPC (network + server's own lnd invoice creation, including hold-invoice persistence) - LightningClient.DecodePaymentRequest - Store.CreateReservation 15 seconds was achievable on a fast LAN with idle servers, but under even modest load (server-side hold-invoice creation can routinely take several seconds in the wild) the timer expired and the RPC returned an error to the caller. The FSM kept running in the background and the wallet would still pay the prepay LN invoice -- so the user got an error, but their funds still moved. The next call to the same reservation_id would then fail mysteriously because the server-side state was already advanced. Bump to 60s. The right longer-term fix is to plumb the caller's gRPC context into the FSM SendEvent so cancellation actually aborts the in-flight server call instead of orphaning it; that's a larger refactor and is left as a follow-up. --- instantout/reservation/manager.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 02c3fda44..fe2952b8f 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -16,7 +16,17 @@ import ( ) var ( - defaultWaitForStateTime = time.Second * 15 + // defaultWaitForStateTime is how long RequestReservationFromServer + // blocks waiting for the FSM to advance to SendPrepaymentPayment. + // The action that drives that transition performs a server RPC + // round-trip that itself creates an lnd hold invoice on the swap + // server side, plus an lnd DecodePaymentRequest, plus a local + // CreateReservation. Under load any one of these can take a few + // seconds, so 15s is too tight: when the RPC times out the FSM + // continues running in the background and may still pay the + // prepay invoice after the caller has been told the request + // failed. 60s gives realistic head-room. + defaultWaitForStateTime = time.Second * 60 ) // FSMSendEventReq contains the information needed to send an event to the FSM. From 10512bfa1973b73fb9b61f65d4e462fc36ec0e07 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 16:43:52 +0200 Subject: [PATCH 30/35] instantout: unlock reservations on OnRecover from in-flight states SendPaymentAndPollAccepted and BuildHtlc both run after PollPaymentAcceptedAction has called LockReservation on every reservation backing the swap. Their OnRecover transitions pointed directly to Failed, whose action is fsm.NoOpAction -- so on daemon restart while in either state, the FSM moved to Failed without ever unlocking the reservations. The local store kept them in the Locked state until on-chain expiry (typically tens of hours later), making them unusable for any subsequent swap. For users who pay for reservations (PR #883's invoice-requested flow) that is a direct material loss. Add an intermediate UnlockReservationsOnRecover state whose action calls handleErrorAndUnlockReservations and then routes to Failed via the normal OnError edge. SendPaymentAndPollAccepted.OnRecover and BuildHtlc.OnRecover now point at this state instead of Failed directly. Init.OnRecover -> Failed is left alone because at that point the InstantOut row has not yet been persisted and no reservation locks have been taken; there is nothing to clean up. Post-PushPreimage states (PushPreimage.OnRecover -> PushPreimage, etc.) are also left alone since they self-loop on recovery rather than terminate. The cleanup helper itself still derives its context from the caller's context (see existing handleErrorAndUnlockReservations); fixing that context-cancel hazard is a separate change. --- instantout/actions.go | 15 +++++++++++++++ instantout/fsm.go | 24 ++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/instantout/actions.go b/instantout/actions.go index 8353ee1e8..6d3f403c5 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -691,6 +691,21 @@ func (f *FSM) WaitForHtlcSweepConfirmedAction(ctx context.Context, } } +// unlockReservationsOnRecoverAction is the action of the +// UnlockReservationsOnRecover state. It is entered via OnRecover from any +// in-flight state where the reservations are already locked, and it unlocks +// them before routing to Failed. Without this, a crash between +// PollPaymentAcceptedAction's LockReservation and the swap reaching a +// terminal state would leave the reservations permanently Locked in the +// local store, blocking any future InstantOut that wants to spend them. +func (f *FSM) unlockReservationsOnRecoverAction(ctx context.Context, + _ fsm.EventContext) fsm.EventType { + + return f.handleErrorAndUnlockReservations( + ctx, errors.New("instant out recovered from in-flight state"), + ) +} + // handleErrorAndUnlockReservations handles an error and unlocks the // reservations. func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, diff --git a/instantout/fsm.go b/instantout/fsm.go index c188b1582..518bd34a1 100644 --- a/instantout/fsm.go +++ b/instantout/fsm.go @@ -85,6 +85,14 @@ var ( // FailedHtlcSweep is the state where the htlc sweep failed. FailedHtlcSweep = fsm.StateType("FailedHtlcSweep") + // UnlockReservationsOnRecover is a transient state entered via + // OnRecover from any in-flight state that had already locked the + // underlying reservations. Its action unlocks them and routes the + // FSM to Failed, so a crash mid-swap does not leave reservations + // stuck Locked in the local store. + UnlockReservationsOnRecover = fsm.StateType( + "UnlockReservationsOnRecover") + // Failed is the state where the swap failed. Failed = fsm.StateType("InstantOutFailed") ) @@ -246,7 +254,11 @@ func (f *FSM) GetV1ReservationStates() fsm.States { Transitions: fsm.Transitions{ OnPaymentAccepted: BuildHtlc, fsm.OnError: Failed, - OnRecover: Failed, + // OnRecover must go through cleanup since + // PollPaymentAcceptedAction has already locked + // the reservations by the time the FSM can + // crash here. + OnRecover: UnlockReservationsOnRecover, }, Action: f.PollPaymentAcceptedAction, }, @@ -254,10 +266,18 @@ func (f *FSM) GetV1ReservationStates() fsm.States { Transitions: fsm.Transitions{ OnHtlcSigReceived: PushPreimage, fsm.OnError: Failed, - OnRecover: Failed, + // Same as SendPaymentAndPollAccepted -- the + // reservations are still locked at this point. + OnRecover: UnlockReservationsOnRecover, }, Action: f.BuildHTLCAction, }, + UnlockReservationsOnRecover: fsm.State{ + Transitions: fsm.Transitions{ + fsm.OnError: Failed, + }, + Action: f.unlockReservationsOnRecoverAction, + }, PushPreimage: fsm.State{ Transitions: fsm.Transitions{ OnSweeplessSweepPublished: WaitForSweeplessSweepConfirmed, From 29c92a10ea77accca068a7612931523de012c190 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 16:56:42 +0200 Subject: [PATCH 31/35] instantout: detach reservation unlock cleanup context handleErrorAndUnlockReservations is called specifically from error paths and from the new OnRecover cleanup. In practice the caller's ctx is almost always already canceled by the time we get here (caller timeout, daemon shutdown, ctx.Done() arm in PollPaymentAcceptedAction, etc.). The existing implementation derived its 30s timeout context from that canceled parent, so: - The for-loop calling UnlockReservation immediately hit ctx.Err() == context.Canceled on every reservation. Locks were never released on disk. - The goroutine sending CancelInstantSwap to the server captured the same already-canceled ctx, then further wrapped it in WithTimeout (still canceled). The server never heard about the cancel. Both code paths were no-ops in exactly the scenario they were written for. Switch to context.Background() with a fresh 30s timeout so the cleanup actually runs. The goroutine also gets its own background context (the previous code captured the parent's already-done ctx via closure, then re-wrapped it). --- instantout/actions.go | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/instantout/actions.go b/instantout/actions.go index 6d3f403c5..778209281 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -708,11 +708,17 @@ func (f *FSM) unlockReservationsOnRecoverAction(ctx context.Context, // handleErrorAndUnlockReservations handles an error and unlocks the // reservations. -func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, +func (f *FSM) handleErrorAndUnlockReservations(_ context.Context, err error) fsm.EventType { - // We might get here from a canceled context, we create a new context - // with a timeout to unlock the reservations. - ctx, cancel := context.WithTimeout(ctx, time.Second*30) + // We very likely got here from a canceled parent context (caller + // timeout, daemon shutdown, etc.). Deriving with timeout from a + // canceled parent yields an already-done context, so neither the + // local UnlockReservation calls nor the server-side CancelInstantSwap + // RPC would ever get a chance to run. Detach from the caller's + // context entirely. + ctx, cancel := context.WithTimeout( + context.Background(), time.Second*30, + ) defer cancel() // Unlock the reservations. @@ -727,19 +733,23 @@ func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, } // We're also sending the server a cancel message so that it can - // release the reservations. This can be done in a goroutine as we - // wan't to fail the fsm early. + // release the reservations. This runs in a goroutine because we + // want to fail the FSM early -- but it must use its OWN background + // context with timeout, not derive from the cancel above (which + // fires the moment this function returns). go func() { - ctx, cancel := context.WithTimeout(ctx, time.Second*30) + cancelCtx, cancel := context.WithTimeout( + context.Background(), time.Second*30, + ) defer cancel() _, cancelErr := f.cfg.InstantOutClient.CancelInstantSwap( - ctx, &swapserverrpc.CancelInstantSwapRequest{ + cancelCtx, &swapserverrpc.CancelInstantSwapRequest{ SwapHash: f.InstantOut.SwapHash[:], }, ) if cancelErr != nil { - // We'll log the error but not return it as we want to return the - // original error. + // We'll log the error but not return it as we want + // to return the original error. f.Debugf("error sending cancel message: %v", cancelErr) } }() From 10a50ef45235eb70f5e7a593f5aa7dabb7ab7aca Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Mon, 11 May 2026 16:58:45 +0200 Subject: [PATCH 32/35] instantout/reservation: lock initial currentHeight write in Run Run wrote m.currentHeight = height without holding the lock, while later writes (newBlockChan case) and reads in RequestReservationFromServer take m.Lock. Daemon startup serializes 'wait for initChan' before serving RPC, so in practice the race window is short, but the race detector flags it -- and on the nautilus side a similar pattern is the most plausible cause of the unit-race CI failure on the buy-reservations head commit. Symmetric fix here keeps the synchronisation rule uniform. --- instantout/reservation/manager.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index fe2952b8f..037e33d38 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -92,7 +92,13 @@ func (m *Manager) Run(ctx context.Context, height int32, runCtx, cancel := context.WithCancel(ctx) defer cancel() + // Take the lock for the initial write so the race detector sees a + // consistent synchronisation rule (later writes in the new-block + // case already lock; concurrent reads in RequestReservationFromServer + // already lock). + m.Lock() m.currentHeight = height + m.Unlock() err := m.RecoverReservations(runCtx) if err != nil { From 08a06a191419e4ab1541c015a417a5e5f0eb2d4c Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 14:08:15 +0200 Subject: [PATCH 33/35] loopdb: renumber reservation prepay migration Current master already uses migration 15 for static address withdrawals. Move the reservation prepay migration to the next available version so database initialization does not reject the duplicate version. --- ...nvoice.down.sql => 000022_reservation_prepay_invoice.down.sql} | 0 ...ay_invoice.up.sql => 000022_reservation_prepay_invoice.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename loopdb/sqlc/migrations/{000015_reservation_prepay_invoice.down.sql => 000022_reservation_prepay_invoice.down.sql} (100%) rename loopdb/sqlc/migrations/{000015_reservation_prepay_invoice.up.sql => 000022_reservation_prepay_invoice.up.sql} (100%) diff --git a/loopdb/sqlc/migrations/000015_reservation_prepay_invoice.down.sql b/loopdb/sqlc/migrations/000022_reservation_prepay_invoice.down.sql similarity index 100% rename from loopdb/sqlc/migrations/000015_reservation_prepay_invoice.down.sql rename to loopdb/sqlc/migrations/000022_reservation_prepay_invoice.down.sql diff --git a/loopdb/sqlc/migrations/000015_reservation_prepay_invoice.up.sql b/loopdb/sqlc/migrations/000022_reservation_prepay_invoice.up.sql similarity index 100% rename from loopdb/sqlc/migrations/000015_reservation_prepay_invoice.up.sql rename to loopdb/sqlc/migrations/000022_reservation_prepay_invoice.up.sql From fb6e6101e8d7cddce54318716a84ba287cdf3505 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 15:01:50 +0200 Subject: [PATCH 34/35] instantout/reservation: adapt hardening tests to protocol FSM The security tests use the pre-feature helper names. Update them for the client-requested reservation manager API after combining both PR stacks. --- instantout/reservation/manager_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index fd7db24b3..7b7280f02 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -123,8 +123,8 @@ func TestManagerContinuesAfterInvalidNotification(t *testing.T) { <-initChan - // A malformed ID is rejected by newReservation. The manager should log - // the error and continue processing the stream. + // A malformed ID is rejected by newReservationFromNtfn. The manager + // should log the error and continue processing the stream. testContext.reservationNotificationChan <- &swapserverrpc.ServerReservationNotification{ ReservationId: []byte{1}, } @@ -163,12 +163,12 @@ func TestManagerRejectsDuplicateReservation(t *testing.T) { defaultExpiry, } - firstFSM, err := testContext.manager.newReservation( + firstFSM, err := testContext.manager.newReservationFromNtfn( ctx, uint32(testContext.mockLnd.Height), req, ) require.NoError(t, err) - secondFSM, err := testContext.manager.newReservation( + secondFSM, err := testContext.manager.newReservationFromNtfn( ctx, uint32(testContext.mockLnd.Height), req, ) require.ErrorIs(t, err, ErrReservationAlreadyExists) @@ -189,11 +189,11 @@ func TestManagerLimitsActiveReservations(t *testing.T) { id[0] = byte(i) id[1] = byte(i >> 8) testContext.manager.activeReservations[id] = NewFSM( - testContext.manager.cfg, + testContext.manager.cfg, ProtocolVersionServerInitiated, ) } - reservationFSM, err := testContext.manager.newReservation( + reservationFSM, err := testContext.manager.newReservationFromNtfn( t.Context(), uint32(testContext.mockLnd.Height), &swapserverrpc.ServerReservationNotification{ ReservationId: defaultReservationId[:], From 696f7984a789f797976a20e1b7f5c72abc09c479 Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Tue, 11 Aug 2026 15:04:37 +0200 Subject: [PATCH 35/35] docs: regenerate reservation CLI reference Regenerate the command reference after adding reservation quote and purchase commands. --- docs/loop.1 | 12 ++++++++++++ docs/loop.md | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/docs/loop.1 b/docs/loop.1 index 1e21905b8..f8f2e729f 100644 --- a/docs/loop.1 +++ b/docs/loop.1 @@ -388,6 +388,18 @@ list all reservations .PP \fB--help, -h\fP: show help +.SS new, n +create a new reservation + +.PP +\fB--amt\fP="": the amount in satoshis for the reservation (default: 0) + +.PP +\fB--expiry\fP="": the relative block height at which the reservation expires (default: 0) + +.PP +\fB--help, -h\fP: show help + .SH instantout perform an instant off-chain to on-chain swap (looping out) diff --git a/docs/loop.md b/docs/loop.md index 316cebdce..ab17754d8 100644 --- a/docs/loop.md +++ b/docs/loop.md @@ -455,6 +455,26 @@ The following flags are supported: |-----------------|-------------|------|:-------------:| | `--help` (`-h`) | show help | bool | `false` | +### `reservations new` subcommand (aliases: `n`) + +create a new reservation. + +Create a new reservation with the given value and expiry. + +Usage: + +```bash +$ loop [GLOBAL FLAGS] reservations new [COMMAND FLAGS] [ARGUMENTS...] +``` + +The following flags are supported: + +| Name | Description | Type | Default value | +|-----------------|------------------------------------------------------------|------|:-------------:| +| `--amt="…"` | the amount in satoshis for the reservation | uint | `0` | +| `--expiry="…"` | the relative block height at which the reservation expires | uint | `0` | +| `--help` (`-h`) | show help | bool | `false` | + ### `instantout` command perform an instant off-chain to on-chain swap (looping out).