mirror of
https://github.com/lightninglabs/loop.git
synced 2026-08-13 12:33:03 +02:00
Merge 696f7984a7 into 1b81a06b42
This commit is contained in:
commit
b02df896b4
41 changed files with 2442 additions and 312 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -2,13 +2,27 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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 +34,7 @@ var reservationsCommands = &cli.Command{
|
|||
`,
|
||||
Commands: []*cli.Command{
|
||||
listReservationsCommand,
|
||||
newReservationCommand,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -34,8 +49,76 @@ 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
|
||||
if _, err := fmt.Scanln(&answer); err != nil ||
|
||||
!strings.EqualFold(answer, "y") {
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,8 @@
|
|||
"Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI="
|
||||
],
|
||||
"outgoing_chan_set": [],
|
||||
"dest_addr": ""
|
||||
"dest_addr": "",
|
||||
"max_swap_fee_sat": "4800"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -148,7 +148,8 @@
|
|||
"outgoing_chan_set": [
|
||||
"125344325763072"
|
||||
],
|
||||
"dest_addr": ""
|
||||
"dest_addr": "",
|
||||
"max_swap_fee_sat": "3200"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -162,7 +162,8 @@
|
|||
"cSfKVONNmsK9+p4Uc5nc3ZtE+37uOODHeq1vprhh/x4="
|
||||
],
|
||||
"outgoing_chan_set": [],
|
||||
"dest_addr": ""
|
||||
"dest_addr": "",
|
||||
"max_swap_fee_sat": "1600"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
12
docs/loop.1
12
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)
|
||||
|
||||
|
|
|
|||
20
docs/loop.md
20
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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,13 @@ 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
|
||||
// resumed after restart.
|
||||
type RecoverInstantOutCtx struct {
|
||||
currentHeight int32
|
||||
}
|
||||
|
||||
// InitInstantOutAction is the first action that is executed when the instant
|
||||
|
|
@ -78,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),
|
||||
|
|
@ -99,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)
|
||||
|
||||
|
|
@ -161,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)
|
||||
|
|
@ -188,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,
|
||||
|
|
@ -206,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,
|
||||
|
|
@ -293,6 +332,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(
|
||||
|
|
@ -373,6 +421,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,
|
||||
|
|
@ -382,6 +450,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)
|
||||
|
|
@ -614,13 +691,34 @@ 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,
|
||||
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.
|
||||
|
|
@ -635,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)
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -57,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
|
||||
|
||||
|
|
@ -112,7 +118,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 +131,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 {
|
||||
|
|
@ -263,12 +298,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 +383,30 @@ 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))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
haveAllSigs, finalSig, err := signer.MuSig2CombineSig(
|
||||
ctx, musig2Sessions[idx].SessionID,
|
||||
[][]byte{serverSigs[idx]},
|
||||
|
|
@ -343,7 +419,26 @@ 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(
|
||||
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
|
||||
|
|
|
|||
203
instantout/instantout_test.go
Normal file
203
instantout/instantout_test.go
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
package instantout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
"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/lightningnetwork/lnd/lnwire"
|
||||
"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
|
||||
}
|
||||
|
||||
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) {
|
||||
_, 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")
|
||||
})
|
||||
}
|
||||
|
||||
// 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})
|
||||
|
||||
sessions := []*input.MuSig2SessionInfo{{}}
|
||||
_, err := instantOut.finalizeMusig2Transaction(
|
||||
context.Background(), &invalidFinalSigSigner{},
|
||||
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)
|
||||
}
|
||||
|
||||
// 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",
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
@ -135,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
|
||||
|
|
@ -148,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()
|
||||
|
|
@ -158,6 +184,7 @@ func (m *Manager) NewInstantOut(ctx context.Context,
|
|||
initationHeight: m.currentHeight,
|
||||
protocolVersion: CurrentProtocolVersion(),
|
||||
sweepAddress: sweepAddr,
|
||||
maxSwapFee: maxSwapFee,
|
||||
}
|
||||
|
||||
instantOut, err := NewFSM(m.cfg, ProtocolVersionFullReservation)
|
||||
|
|
|
|||
|
|
@ -2,16 +2,189 @@ 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. 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",
|
||||
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
|
||||
|
||||
// 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.
|
||||
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 +192,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 +413,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -154,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)
|
||||
}
|
||||
}
|
||||
|
|
@ -203,6 +223,7 @@ func TestSubscribeToConfirmationAction(t *testing.T) {
|
|||
blockHeight int32
|
||||
blockErr error
|
||||
sendTxConf bool
|
||||
outputValue btcutil.Amount
|
||||
confErr error
|
||||
expectedEvent fsm.EventType
|
||||
}{
|
||||
|
|
@ -210,8 +231,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 +301,7 @@ func TestSubscribeToConfirmationAction(t *testing.T) {
|
|||
TxIn: []*wire.TxIn{},
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: int64(defaultValue),
|
||||
Value: int64(tc.outputValue),
|
||||
PkScript: pkScript,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -60,10 +70,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)
|
||||
|
|
@ -82,6 +92,9 @@ func NewFSMFromReservation(cfg *Config, reservation *Reservation) *FSM {
|
|||
case ProtocolVersionServerInitiated:
|
||||
states = reservationFsm.GetServerInitiatedReservationStates()
|
||||
|
||||
case ProtocolVersionClientInitiated:
|
||||
states = reservationFsm.GetClientInitiatedReservationStates()
|
||||
|
||||
default:
|
||||
states = make(fsm.States)
|
||||
}
|
||||
|
|
@ -100,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")
|
||||
|
|
@ -127,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")
|
||||
|
|
@ -160,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 {
|
||||
|
|
@ -176,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{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package reservation
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -10,9 +11,31 @@ 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 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.
|
||||
type FSMSendEventReq struct {
|
||||
fsm *FSM
|
||||
event fsm.EventType
|
||||
eventCtx fsm.EventContext
|
||||
}
|
||||
|
||||
// Manager manages the reservation state machines.
|
||||
type Manager struct {
|
||||
sync.Mutex
|
||||
|
|
@ -23,6 +46,32 @@ 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
|
||||
// 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.
|
||||
|
|
@ -30,6 +79,7 @@ func NewManager(cfg *Config) *Manager {
|
|||
return &Manager{
|
||||
cfg: cfg,
|
||||
activeReservations: make(map[ID]*FSM),
|
||||
reqChan: make(chan *FSMSendEventReq),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +92,13 @@ func (m *Manager) Run(ctx context.Context, height int32,
|
|||
runCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
currentHeight := height
|
||||
// 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 {
|
||||
|
|
@ -64,7 +120,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 {
|
||||
|
|
@ -76,13 +134,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 {
|
||||
return err
|
||||
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
|
||||
|
||||
|
|
@ -93,9 +165,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(
|
||||
|
|
@ -110,17 +184,42 @@ 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)
|
||||
reservationFSM := NewFSM(m.cfg, ProtocolVersionServerInitiated)
|
||||
|
||||
// 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
|
||||
}
|
||||
if len(m.activeReservations) >= maxActiveReservations {
|
||||
m.Unlock()
|
||||
return nil, ErrTooManyActiveReservations
|
||||
}
|
||||
m.activeReservations[reservationID] = reservationFSM
|
||||
m.Unlock()
|
||||
|
||||
initContext := &InitReservationContext{
|
||||
reservationFSM.RegisterObserver(&finalStateObserver{
|
||||
manager: m,
|
||||
id: reservationID,
|
||||
fsm: reservationFSM,
|
||||
})
|
||||
|
||||
initContext := &ServerRequestedInitContext{
|
||||
reservationID: reservationID,
|
||||
serverPubkey: serverKey,
|
||||
value: btcutil.Amount(req.Value),
|
||||
|
|
@ -130,9 +229,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)
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
@ -143,6 +244,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",
|
||||
|
|
@ -154,6 +261,73 @@ 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. 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
|
||||
// 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 {
|
||||
|
|
@ -162,6 +336,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
|
||||
|
|
@ -174,6 +358,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.
|
||||
|
|
|
|||
|
|
@ -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[:],
|
||||
|
|
@ -57,6 +57,7 @@ func TestManager(t *testing.T) {
|
|||
confTx := &wire.MsgTx{
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: int64(defaultValue),
|
||||
PkScript: pkScript,
|
||||
},
|
||||
},
|
||||
|
|
@ -97,6 +98,117 @@ 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
|
||||
// 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 newReservationFromNtfn. 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)
|
||||
}
|
||||
|
||||
// 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.newReservationFromNtfn(
|
||||
ctx, uint32(testContext.mockLnd.Height), req,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
secondFSM, err := testContext.manager.newReservationFromNtfn(
|
||||
ctx, uint32(testContext.mockLnd.Height), req,
|
||||
)
|
||||
require.ErrorIs(t, err, ErrReservationAlreadyExists)
|
||||
require.Nil(t, secondFSM)
|
||||
require.Same(
|
||||
t, firstFSM,
|
||||
testContext.manager.activeReservations[defaultReservationId],
|
||||
)
|
||||
}
|
||||
|
||||
// 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, ProtocolVersionServerInitiated,
|
||||
)
|
||||
}
|
||||
|
||||
reservationFSM, err := testContext.manager.newReservationFromNtfn(
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -142,8 +145,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 +160,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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
@ -180,6 +181,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
|
||||
}
|
||||
|
||||
|
|
@ -289,6 +294,7 @@ func sqlReservationToReservation(row sqlc.Reservation,
|
|||
InitiationHeight: row.InitiationHeight,
|
||||
State: fsm.StateType(lastUpdate.UpdateState),
|
||||
ProtocolVersion: ProtocolVersion(row.ProtocolVersion),
|
||||
PrepayInvoice: row.PrepayInvoice,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -1739,6 +1739,49 @@ func (s *swapClientServer) ListReservations(ctx context.Context,
|
|||
}, nil
|
||||
}
|
||||
|
||||
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),
|
||||
)
|
||||
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) {
|
||||
|
||||
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,
|
||||
)
|
||||
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,
|
||||
|
|
@ -1765,6 +1808,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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
ALTER TABLE reservations ADD COLUMN protocol_version INTEGER NOT NULL DEFAULT 0;
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE reservations DROP COLUMN prepay_invoice;
|
||||
|
|
@ -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 '';
|
||||
|
|
@ -115,6 +115,7 @@ type Reservation struct {
|
|||
OutIndex sql.NullInt32
|
||||
ConfirmationHeight sql.NullInt32
|
||||
ProtocolVersion int32
|
||||
PrepayInvoice string
|
||||
}
|
||||
|
||||
type ReservationUpdate struct {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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.
|
||||
|
|
@ -1685,6 +1738,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 {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -2616,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": {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -177,18 +177,41 @@ var RequiredPermissions = map[string][]bakery.Op{
|
|||
"/looprpc.SwapClient/ListReservations": {{
|
||||
Entity: "swap",
|
||||
Action: "read",
|
||||
}, {
|
||||
Entity: "loop",
|
||||
Action: "out",
|
||||
}},
|
||||
"/looprpc.SwapClient/ReservationRequest": {{
|
||||
Entity: "swap",
|
||||
Action: "execute",
|
||||
}, {
|
||||
Entity: "loop",
|
||||
Action: "out",
|
||||
}},
|
||||
"/looprpc.SwapClient/ReservationQuote": {{
|
||||
Entity: "swap",
|
||||
Action: "read",
|
||||
}},
|
||||
"/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",
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue