mirror of https://github.com/lightninglabs/loop
Merge pull request #632 from sputn1ck/instantloopout_2
[2/?] Instant loop out: Add reservationsupdate-to-v0.27.0-beta
commit
e9d374a341
@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/lightninglabs/loop/looprpc"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reservationsCommands = cli.Command{
|
||||||
|
|
||||||
|
Name: "reservations",
|
||||||
|
ShortName: "r",
|
||||||
|
Usage: "manage reservations",
|
||||||
|
Description: `
|
||||||
|
With loopd running, you can use this command to manage your
|
||||||
|
reservations. Reservations are 2-of-2 multisig utxos that
|
||||||
|
the loop server can open to clients. The reservations are used
|
||||||
|
to enable instant swaps.
|
||||||
|
`,
|
||||||
|
Subcommands: []cli.Command{
|
||||||
|
listReservationsCommand,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
listReservationsCommand = cli.Command{
|
||||||
|
Name: "list",
|
||||||
|
ShortName: "l",
|
||||||
|
Usage: "list all reservations",
|
||||||
|
ArgsUsage: "",
|
||||||
|
Description: `
|
||||||
|
List all reservations.
|
||||||
|
`,
|
||||||
|
Action: listReservations,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func listReservations(ctx *cli.Context) error {
|
||||||
|
client, cleanup, err := getClient(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
resp, err := client.ListReservations(
|
||||||
|
context.Background(), &looprpc.ListReservationsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
printRespJSON(resp)
|
||||||
|
return nil
|
||||||
|
}
|
@ -0,0 +1,176 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
looprpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitReservationContext contains the request parameters for a reservation.
|
||||||
|
type InitReservationContext struct {
|
||||||
|
reservationID ID
|
||||||
|
serverPubkey *btcec.PublicKey
|
||||||
|
value btcutil.Amount
|
||||||
|
expiry uint32
|
||||||
|
heightHint uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitAction is the action that is executed when the reservation state machine
|
||||||
|
// is initialized. It creates the reservation in the database and dispatches the
|
||||||
|
// payment to the server.
|
||||||
|
func (r *FSM) InitAction(eventCtx fsm.EventContext) fsm.EventType {
|
||||||
|
// Check if the context is of the correct type.
|
||||||
|
reservationRequest, ok := eventCtx.(*InitReservationContext)
|
||||||
|
if !ok {
|
||||||
|
return r.HandleError(fsm.ErrInvalidContextType)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyRes, err := r.cfg.Wallet.DeriveNextKey(
|
||||||
|
r.ctx, KeyFamily,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the client reservation details to the server.
|
||||||
|
log.Debugf("Dispatching reservation to server: %x",
|
||||||
|
reservationRequest.reservationID)
|
||||||
|
|
||||||
|
request := &looprpc.ServerOpenReservationRequest{
|
||||||
|
ReservationId: reservationRequest.reservationID[:],
|
||||||
|
ClientKey: keyRes.PubKey.SerializeCompressed(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.cfg.ReservationClient.OpenReservation(r.ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
reservation, err := NewReservation(
|
||||||
|
reservationRequest.reservationID,
|
||||||
|
reservationRequest.serverPubkey,
|
||||||
|
keyRes.PubKey,
|
||||||
|
reservationRequest.value,
|
||||||
|
reservationRequest.expiry,
|
||||||
|
reservationRequest.heightHint,
|
||||||
|
keyRes.KeyLocator,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.reservation = reservation
|
||||||
|
|
||||||
|
// Create the reservation in the database.
|
||||||
|
err = r.cfg.Store.CreateReservation(r.ctx, reservation)
|
||||||
|
if err != nil {
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return OnBroadcast
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeToConfirmationAction is the action that is executed when the
|
||||||
|
// reservation is waiting for confirmation. It subscribes to the confirmation
|
||||||
|
// of the reservation transaction.
|
||||||
|
func (r *FSM) SubscribeToConfirmationAction(_ fsm.EventContext) fsm.EventType {
|
||||||
|
pkscript, err := r.reservation.GetPkScript()
|
||||||
|
if err != nil {
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
callCtx, cancel := context.WithCancel(r.ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Subscribe to the confirmation of the reservation transaction.
|
||||||
|
log.Debugf("Subscribing to conf for reservation: %x pkscript: %x, "+
|
||||||
|
"initiation height: %v", r.reservation.ID, pkscript,
|
||||||
|
r.reservation.InitiationHeight)
|
||||||
|
|
||||||
|
confChan, errConfChan, err := r.cfg.ChainNotifier.RegisterConfirmationsNtfn(
|
||||||
|
callCtx, nil, pkscript, DefaultConfTarget,
|
||||||
|
r.reservation.InitiationHeight,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
r.Errorf("unable to subscribe to conf notification: %v", err)
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockChan, errBlockChan, err := r.cfg.ChainNotifier.RegisterBlockEpochNtfn(
|
||||||
|
callCtx,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
r.Errorf("unable to subscribe to block notifications: %v", err)
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now wait for the confirmation of the reservation transaction.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-errConfChan:
|
||||||
|
r.Errorf("conf subscription error: %v", err)
|
||||||
|
return r.HandleError(err)
|
||||||
|
|
||||||
|
case err := <-errBlockChan:
|
||||||
|
r.Errorf("block subscription error: %v", err)
|
||||||
|
return r.HandleError(err)
|
||||||
|
|
||||||
|
case confInfo := <-confChan:
|
||||||
|
r.Debugf("reservation confirmed: %v", confInfo)
|
||||||
|
outpoint, err := r.reservation.findReservationOutput(
|
||||||
|
confInfo.Tx,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.reservation.ConfirmationHeight = confInfo.BlockHeight
|
||||||
|
r.reservation.Outpoint = outpoint
|
||||||
|
|
||||||
|
return OnConfirmed
|
||||||
|
|
||||||
|
case block := <-blockChan:
|
||||||
|
r.Debugf("block received: %v expiry: %v", block,
|
||||||
|
r.reservation.Expiry)
|
||||||
|
|
||||||
|
if uint32(block) >= r.reservation.Expiry {
|
||||||
|
return OnTimedOut
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-r.ctx.Done():
|
||||||
|
return fsm.NoOp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReservationConfirmedAction waits for the reservation to be either expired or
|
||||||
|
// waits for other actions to happen.
|
||||||
|
func (r *FSM) ReservationConfirmedAction(_ fsm.EventContext) fsm.EventType {
|
||||||
|
blockHeightChan, errEpochChan, err := r.cfg.ChainNotifier.
|
||||||
|
RegisterBlockEpochNtfn(r.ctx)
|
||||||
|
if err != nil {
|
||||||
|
return r.HandleError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err := <-errEpochChan:
|
||||||
|
return r.HandleError(err)
|
||||||
|
|
||||||
|
case blockHeight := <-blockHeightChan:
|
||||||
|
expired := blockHeight >= int32(r.reservation.Expiry)
|
||||||
|
if expired {
|
||||||
|
r.Debugf("Reservation %v expired",
|
||||||
|
r.reservation.ID)
|
||||||
|
|
||||||
|
return OnTimedOut
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-r.ctx.Done():
|
||||||
|
return fsm.NoOp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,371 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/lndclient"
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
"github.com/lightninglabs/loop/swapserverrpc"
|
||||||
|
"github.com/lightninglabs/loop/test"
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultPubkeyBytes, _ = hex.DecodeString("021c97a90a411ff2b10dc2a8e32de2f29d2fa49d41bfbb52bd416e460db0747d0d")
|
||||||
|
defaultPubkey, _ = btcec.ParsePubKey(defaultPubkeyBytes)
|
||||||
|
|
||||||
|
defaultValue = btcutil.Amount(100)
|
||||||
|
|
||||||
|
defaultExpiry = uint32(100)
|
||||||
|
)
|
||||||
|
|
||||||
|
func newValidInitReservationContext() *InitReservationContext {
|
||||||
|
return &InitReservationContext{
|
||||||
|
reservationID: ID{0x01},
|
||||||
|
serverPubkey: defaultPubkey,
|
||||||
|
value: defaultValue,
|
||||||
|
expiry: defaultExpiry,
|
||||||
|
heightHint: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newValidClientReturn() *swapserverrpc.ServerOpenReservationResponse {
|
||||||
|
return &swapserverrpc.ServerOpenReservationResponse{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockReservationClient struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReservationClient) OpenReservation(ctx context.Context,
|
||||||
|
in *swapserverrpc.ServerOpenReservationRequest,
|
||||||
|
opts ...grpc.CallOption) (*swapserverrpc.ServerOpenReservationResponse,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
args := m.Called(ctx, in, opts)
|
||||||
|
return args.Get(0).(*swapserverrpc.ServerOpenReservationResponse),
|
||||||
|
args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReservationClient) ReservationNotificationStream(
|
||||||
|
ctx context.Context, in *swapserverrpc.ReservationNotificationRequest,
|
||||||
|
opts ...grpc.CallOption,
|
||||||
|
) (swapserverrpc.ReservationService_ReservationNotificationStreamClient,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
args := m.Called(ctx, in, opts)
|
||||||
|
return args.Get(0).(swapserverrpc.ReservationService_ReservationNotificationStreamClient),
|
||||||
|
args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockReservationClient) FetchL402(ctx context.Context,
|
||||||
|
in *swapserverrpc.FetchL402Request,
|
||||||
|
opts ...grpc.CallOption) (*swapserverrpc.FetchL402Response, error) {
|
||||||
|
|
||||||
|
args := m.Called(ctx, in, opts)
|
||||||
|
|
||||||
|
return args.Get(0).(*swapserverrpc.FetchL402Response),
|
||||||
|
args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockStore struct {
|
||||||
|
mock.Mock
|
||||||
|
|
||||||
|
Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockStore) CreateReservation(ctx context.Context,
|
||||||
|
reservation *Reservation) error {
|
||||||
|
|
||||||
|
args := m.Called(ctx, reservation)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInitReservationAction tests the InitReservationAction of the reservation
|
||||||
|
// state machine.
|
||||||
|
func TestInitReservationAction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
eventCtx fsm.EventContext
|
||||||
|
mockStoreErr error
|
||||||
|
mockClientReturn *swapserverrpc.ServerOpenReservationResponse
|
||||||
|
mockClientErr error
|
||||||
|
expectedEvent fsm.EventType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
eventCtx: newValidInitReservationContext(),
|
||||||
|
mockClientReturn: newValidClientReturn(),
|
||||||
|
expectedEvent: OnBroadcast,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid context",
|
||||||
|
eventCtx: struct{}{},
|
||||||
|
expectedEvent: fsm.OnError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reservation server error",
|
||||||
|
eventCtx: newValidInitReservationContext(),
|
||||||
|
mockClientErr: errors.New("reservation server error"),
|
||||||
|
expectedEvent: fsm.OnError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "store error",
|
||||||
|
eventCtx: newValidInitReservationContext(),
|
||||||
|
mockClientReturn: newValidClientReturn(),
|
||||||
|
mockStoreErr: errors.New("store error"),
|
||||||
|
expectedEvent: fsm.OnError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
ctxb := context.Background()
|
||||||
|
mockLnd := test.NewMockLnd()
|
||||||
|
mockReservationClient := new(mockReservationClient)
|
||||||
|
mockReservationClient.On(
|
||||||
|
"OpenReservation", mock.Anything,
|
||||||
|
mock.Anything, mock.Anything,
|
||||||
|
).Return(tc.mockClientReturn, tc.mockClientErr)
|
||||||
|
|
||||||
|
mockStore := new(mockStore)
|
||||||
|
mockStore.On(
|
||||||
|
"CreateReservation", mock.Anything, mock.Anything,
|
||||||
|
).Return(tc.mockStoreErr)
|
||||||
|
|
||||||
|
reservationFSM := &FSM{
|
||||||
|
ctx: ctxb,
|
||||||
|
cfg: &Config{
|
||||||
|
Wallet: mockLnd.WalletKit,
|
||||||
|
ChainNotifier: mockLnd.ChainNotifier,
|
||||||
|
ReservationClient: mockReservationClient,
|
||||||
|
Store: mockStore,
|
||||||
|
},
|
||||||
|
StateMachine: &fsm.StateMachine{},
|
||||||
|
}
|
||||||
|
|
||||||
|
event := reservationFSM.InitAction(tc.eventCtx)
|
||||||
|
require.Equal(t, tc.expectedEvent, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockChainNotifier struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockChainNotifier) RegisterConfirmationsNtfn(ctx context.Context,
|
||||||
|
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32,
|
||||||
|
options ...lndclient.NotifierOption) (chan *chainntnfs.TxConfirmation,
|
||||||
|
chan error, error) {
|
||||||
|
|
||||||
|
args := m.Called(ctx, txid, pkScript, numConfs, heightHint)
|
||||||
|
return args.Get(0).(chan *chainntnfs.TxConfirmation), args.Get(1).(chan error), args.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockChainNotifier) RegisterBlockEpochNtfn(ctx context.Context) (
|
||||||
|
chan int32, chan error, error) {
|
||||||
|
|
||||||
|
args := m.Called(ctx)
|
||||||
|
return args.Get(0).(chan int32), args.Get(1).(chan error), args.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockChainNotifier) RegisterSpendNtfn(ctx context.Context,
|
||||||
|
outpoint *wire.OutPoint, pkScript []byte, heightHint int32) (
|
||||||
|
chan *chainntnfs.SpendDetail, chan error, error) {
|
||||||
|
|
||||||
|
args := m.Called(ctx, pkScript, heightHint)
|
||||||
|
return args.Get(0).(chan *chainntnfs.SpendDetail), args.Get(1).(chan error), args.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSubscribeToConfirmationAction tests the SubscribeToConfirmationAction of
|
||||||
|
// the reservation state machine.
|
||||||
|
func TestSubscribeToConfirmationAction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
blockHeight int32
|
||||||
|
blockErr error
|
||||||
|
sendTxConf bool
|
||||||
|
confErr error
|
||||||
|
expectedEvent fsm.EventType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "success",
|
||||||
|
blockHeight: 0,
|
||||||
|
sendTxConf: true,
|
||||||
|
expectedEvent: OnConfirmed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "expired",
|
||||||
|
blockHeight: 100,
|
||||||
|
expectedEvent: OnTimedOut,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "block error",
|
||||||
|
blockHeight: 0,
|
||||||
|
blockErr: errors.New("block error"),
|
||||||
|
expectedEvent: fsm.OnError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tx confirmation error",
|
||||||
|
blockHeight: 0,
|
||||||
|
confErr: errors.New("tx confirmation error"),
|
||||||
|
expectedEvent: fsm.OnError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
chainNotifier := new(MockChainNotifier)
|
||||||
|
|
||||||
|
// Create the FSM.
|
||||||
|
r := NewFSMFromReservation(
|
||||||
|
context.Background(), &Config{
|
||||||
|
ChainNotifier: chainNotifier,
|
||||||
|
},
|
||||||
|
&Reservation{
|
||||||
|
Expiry: defaultExpiry,
|
||||||
|
ServerPubkey: defaultPubkey,
|
||||||
|
ClientPubkey: defaultPubkey,
|
||||||
|
Value: defaultValue,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pkScript, err := r.reservation.GetPkScript()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
confChan := make(chan *chainntnfs.TxConfirmation)
|
||||||
|
confErrChan := make(chan error)
|
||||||
|
blockChan := make(chan int32)
|
||||||
|
blockErrChan := make(chan error)
|
||||||
|
|
||||||
|
// Define the expected return values for the mocks.
|
||||||
|
chainNotifier.On(
|
||||||
|
"RegisterConfirmationsNtfn", mock.Anything, mock.Anything,
|
||||||
|
mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(confChan, confErrChan, nil)
|
||||||
|
|
||||||
|
chainNotifier.On("RegisterBlockEpochNtfn", mock.Anything).Return(
|
||||||
|
blockChan, blockErrChan, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Send the tx confirmation.
|
||||||
|
if tc.sendTxConf {
|
||||||
|
confChan <- &chainntnfs.TxConfirmation{
|
||||||
|
Tx: &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{},
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: int64(defaultValue),
|
||||||
|
PkScript: pkScript,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Send the block notification.
|
||||||
|
if tc.blockHeight != 0 {
|
||||||
|
blockChan <- tc.blockHeight
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Send the block notification error.
|
||||||
|
if tc.blockErr != nil {
|
||||||
|
blockErrChan <- tc.blockErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Send the tx confirmation error.
|
||||||
|
if tc.confErr != nil {
|
||||||
|
confErrChan <- tc.confErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
eventType := r.SubscribeToConfirmationAction(nil)
|
||||||
|
// Assert that the return value is as expected
|
||||||
|
require.Equal(t, tc.expectedEvent, eventType)
|
||||||
|
|
||||||
|
// Assert that the expected functions were called on the mocks
|
||||||
|
chainNotifier.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReservationConfirmedAction tests the ReservationConfirmedAction of the
|
||||||
|
// reservation state machine.
|
||||||
|
func TestReservationConfirmedAction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
blockHeight int32
|
||||||
|
blockErr error
|
||||||
|
expectedEvent fsm.EventType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "expired",
|
||||||
|
blockHeight: 100,
|
||||||
|
expectedEvent: OnTimedOut,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "block error",
|
||||||
|
blockHeight: 0,
|
||||||
|
blockErr: errors.New("block error"),
|
||||||
|
expectedEvent: fsm.OnError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
chainNotifier := new(MockChainNotifier)
|
||||||
|
|
||||||
|
// Create the FSM.
|
||||||
|
r := NewFSMFromReservation(
|
||||||
|
context.Background(), &Config{
|
||||||
|
ChainNotifier: chainNotifier,
|
||||||
|
},
|
||||||
|
&Reservation{
|
||||||
|
Expiry: defaultExpiry,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
blockChan := make(chan int32)
|
||||||
|
blockErrChan := make(chan error)
|
||||||
|
|
||||||
|
// Define our expected return values for the mocks.
|
||||||
|
chainNotifier.On("RegisterBlockEpochNtfn", mock.Anything).Return(
|
||||||
|
blockChan, blockErrChan, nil,
|
||||||
|
)
|
||||||
|
go func() {
|
||||||
|
// Send the block notification.
|
||||||
|
if tc.blockHeight != 0 {
|
||||||
|
blockChan <- tc.blockHeight
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Send the block notification error.
|
||||||
|
if tc.blockErr != nil {
|
||||||
|
blockErrChan <- tc.blockErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
eventType := r.ReservationConfirmedAction(nil)
|
||||||
|
require.Equal(t, tc.expectedEvent, eventType)
|
||||||
|
|
||||||
|
// Assert that the expected functions were called on the mocks
|
||||||
|
chainNotifier.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,229 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/lightninglabs/lndclient"
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
looprpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// defaultObserverSize is the size of the fsm observer channel.
|
||||||
|
defaultObserverSize = 15
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains all the services that the reservation FSM needs to operate.
|
||||||
|
type Config struct {
|
||||||
|
// Store is the database store for the reservations.
|
||||||
|
Store Store
|
||||||
|
|
||||||
|
// Wallet handles the key derivation for the reservation.
|
||||||
|
Wallet lndclient.WalletKitClient
|
||||||
|
|
||||||
|
// ChainNotifier is used to subscribe to block notifications.
|
||||||
|
ChainNotifier lndclient.ChainNotifierClient
|
||||||
|
|
||||||
|
// ReservationClient is the client used to communicate with the
|
||||||
|
// swap server.
|
||||||
|
ReservationClient looprpc.ReservationServiceClient
|
||||||
|
|
||||||
|
// FetchL402 is the function used to fetch the l402 token.
|
||||||
|
FetchL402 func(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FSM is the state machine that manages the reservation lifecycle.
|
||||||
|
type FSM struct {
|
||||||
|
*fsm.StateMachine
|
||||||
|
|
||||||
|
cfg *Config
|
||||||
|
|
||||||
|
reservation *Reservation
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFSM creates a new reservation FSM.
|
||||||
|
func NewFSM(ctx context.Context, cfg *Config) *FSM {
|
||||||
|
reservation := &Reservation{
|
||||||
|
State: fsm.EmptyState,
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewFSMFromReservation(ctx, cfg, reservation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFSMFromReservation creates a new reservation FSM from an existing
|
||||||
|
// reservation recovered from the database.
|
||||||
|
func NewFSMFromReservation(ctx context.Context, cfg *Config,
|
||||||
|
reservation *Reservation) *FSM {
|
||||||
|
|
||||||
|
reservationFsm := &FSM{
|
||||||
|
ctx: ctx,
|
||||||
|
cfg: cfg,
|
||||||
|
reservation: reservation,
|
||||||
|
}
|
||||||
|
|
||||||
|
reservationFsm.StateMachine = fsm.NewStateMachineWithState(
|
||||||
|
reservationFsm.GetReservationStates(), reservation.State,
|
||||||
|
defaultObserverSize,
|
||||||
|
)
|
||||||
|
reservationFsm.ActionEntryFunc = reservationFsm.updateReservation
|
||||||
|
|
||||||
|
return reservationFsm
|
||||||
|
}
|
||||||
|
|
||||||
|
// States.
|
||||||
|
var (
|
||||||
|
// Init is the initial state of the reservation.
|
||||||
|
Init = fsm.StateType("Init")
|
||||||
|
|
||||||
|
// WaitForConfirmation is the state where we wait for the reservation
|
||||||
|
// tx to be confirmed.
|
||||||
|
WaitForConfirmation = fsm.StateType("WaitForConfirmation")
|
||||||
|
|
||||||
|
// Confirmed is the state where the reservation tx has been confirmed.
|
||||||
|
Confirmed = fsm.StateType("Confirmed")
|
||||||
|
|
||||||
|
// TimedOut is the state where the reservation has timed out.
|
||||||
|
TimedOut = fsm.StateType("TimedOut")
|
||||||
|
|
||||||
|
// Failed is the state where the reservation has failed.
|
||||||
|
Failed = fsm.StateType("Failed")
|
||||||
|
|
||||||
|
// Spent is the state where a spend tx has been confirmed.
|
||||||
|
Spent = fsm.StateType("Spent")
|
||||||
|
|
||||||
|
// Locked is the state where the reservation is locked and can't be
|
||||||
|
// used for instant out swaps.
|
||||||
|
Locked = fsm.StateType("Locked")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Events.
|
||||||
|
var (
|
||||||
|
// OnServerRequest is the event that is triggered when the server
|
||||||
|
// requests a new reservation.
|
||||||
|
OnServerRequest = fsm.EventType("OnServerRequest")
|
||||||
|
|
||||||
|
// OnBroadcast is the event that is triggered when the reservation tx
|
||||||
|
// has been broadcast.
|
||||||
|
OnBroadcast = fsm.EventType("OnBroadcast")
|
||||||
|
|
||||||
|
// OnConfirmed is the event that is triggered when the reservation tx
|
||||||
|
// has been confirmed.
|
||||||
|
OnConfirmed = fsm.EventType("OnConfirmed")
|
||||||
|
|
||||||
|
// OnTimedOut is the event that is triggered when the reservation has
|
||||||
|
// timed out.
|
||||||
|
OnTimedOut = fsm.EventType("OnTimedOut")
|
||||||
|
|
||||||
|
// OnSwept is the event that is triggered when the reservation has been
|
||||||
|
// swept by the server.
|
||||||
|
OnSwept = fsm.EventType("OnSwept")
|
||||||
|
|
||||||
|
// OnRecover is the event that is triggered when the reservation FSM
|
||||||
|
// recovers from a restart.
|
||||||
|
OnRecover = fsm.EventType("OnRecover")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetReservationStates returns the statemap that defines the reservation
|
||||||
|
// state machine.
|
||||||
|
func (f *FSM) GetReservationStates() fsm.States {
|
||||||
|
return fsm.States{
|
||||||
|
fsm.EmptyState: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnServerRequest: Init,
|
||||||
|
},
|
||||||
|
Action: nil,
|
||||||
|
},
|
||||||
|
Init: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnBroadcast: WaitForConfirmation,
|
||||||
|
OnRecover: Failed,
|
||||||
|
fsm.OnError: Failed,
|
||||||
|
},
|
||||||
|
Action: f.InitAction,
|
||||||
|
},
|
||||||
|
WaitForConfirmation: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnRecover: WaitForConfirmation,
|
||||||
|
OnConfirmed: Confirmed,
|
||||||
|
OnTimedOut: TimedOut,
|
||||||
|
},
|
||||||
|
Action: f.SubscribeToConfirmationAction,
|
||||||
|
},
|
||||||
|
Confirmed: fsm.State{
|
||||||
|
Transitions: fsm.Transitions{
|
||||||
|
OnTimedOut: TimedOut,
|
||||||
|
OnRecover: Confirmed,
|
||||||
|
},
|
||||||
|
Action: f.ReservationConfirmedAction,
|
||||||
|
},
|
||||||
|
TimedOut: fsm.State{
|
||||||
|
Action: fsm.NoOpAction,
|
||||||
|
},
|
||||||
|
Failed: fsm.State{
|
||||||
|
Action: fsm.NoOpAction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateReservation updates the reservation in the database. This function
|
||||||
|
// is called after every new state transition.
|
||||||
|
func (r *FSM) updateReservation(notification fsm.Notification) {
|
||||||
|
if r.reservation == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Debugf(
|
||||||
|
"NextState: %v, PreviousState: %v, Event: %v",
|
||||||
|
notification.NextState, notification.PreviousState,
|
||||||
|
notification.Event,
|
||||||
|
)
|
||||||
|
|
||||||
|
r.reservation.State = notification.NextState
|
||||||
|
|
||||||
|
// Don't update the reservation if we are in an initial state or if we
|
||||||
|
// are transitioning from an initial state to a failed state.
|
||||||
|
if r.reservation.State == fsm.EmptyState ||
|
||||||
|
r.reservation.State == Init ||
|
||||||
|
(notification.PreviousState == Init &&
|
||||||
|
r.reservation.State == Failed) {
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := r.cfg.Store.UpdateReservation(r.ctx, r.reservation)
|
||||||
|
if err != nil {
|
||||||
|
r.Errorf("unable to update reservation: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FSM) Infof(format string, args ...interface{}) {
|
||||||
|
log.Infof(
|
||||||
|
"Reservation %x: "+format,
|
||||||
|
append([]interface{}{r.reservation.ID}, args...)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FSM) Debugf(format string, args ...interface{}) {
|
||||||
|
log.Debugf(
|
||||||
|
"Reservation %x: "+format,
|
||||||
|
append([]interface{}{r.reservation.ID}, args...)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FSM) Errorf(format string, args ...interface{}) {
|
||||||
|
log.Errorf(
|
||||||
|
"Reservation %x: "+format,
|
||||||
|
append([]interface{}{r.reservation.ID}, args...)...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isFinalState returns true if the state is a final state.
|
||||||
|
func isFinalState(state fsm.StateType) bool {
|
||||||
|
switch state {
|
||||||
|
case Failed, TimedOut, Spent:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrReservationAlreadyExists = fmt.Errorf("reservation already exists")
|
||||||
|
ErrReservationNotFound = fmt.Errorf("reservation not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
KeyFamily = int32(42068)
|
||||||
|
DefaultConfTarget = int32(3)
|
||||||
|
IdLength = 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store is the interface that stores the reservations.
|
||||||
|
type Store interface {
|
||||||
|
// CreateReservation stores the reservation in the database.
|
||||||
|
CreateReservation(ctx context.Context, reservation *Reservation) error
|
||||||
|
|
||||||
|
// UpdateReservation updates the reservation in the database.
|
||||||
|
UpdateReservation(ctx context.Context, reservation *Reservation) error
|
||||||
|
|
||||||
|
// GetReservation retrieves the reservation from the database.
|
||||||
|
GetReservation(ctx context.Context, id ID) (*Reservation, error)
|
||||||
|
|
||||||
|
// ListReservations lists all existing reservations the client has ever
|
||||||
|
// made.
|
||||||
|
ListReservations(ctx context.Context) ([]*Reservation, error)
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/btcsuite/btclog"
|
||||||
|
"github.com/lightningnetwork/lnd/build"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subsystem defines the sub system name of this package.
|
||||||
|
const Subsystem = "RSRV"
|
||||||
|
|
||||||
|
// log is a logger that is initialized with no output filters. This
|
||||||
|
// means the package will not perform any logging by default until the caller
|
||||||
|
// requests it.
|
||||||
|
var log btclog.Logger
|
||||||
|
|
||||||
|
// The default amount of logging is none.
|
||||||
|
func init() {
|
||||||
|
UseLogger(build.NewSubLogger(Subsystem, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseLogger uses a specified Logger to output package logging info.
|
||||||
|
// This should be used in preference to SetLogWriter if the caller is also
|
||||||
|
// using btclog.
|
||||||
|
func UseLogger(logger btclog.Logger) {
|
||||||
|
log = logger
|
||||||
|
}
|
@ -0,0 +1,271 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
reservationrpc "github.com/lightninglabs/loop/swapserverrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manager manages the reservation state machines.
|
||||||
|
type Manager struct {
|
||||||
|
// cfg contains all the services that the reservation manager needs to
|
||||||
|
// operate.
|
||||||
|
cfg *Config
|
||||||
|
|
||||||
|
// activeReservations contains all the active reservationsFSMs.
|
||||||
|
activeReservations map[ID]*FSM
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewManager creates a new reservation manager.
|
||||||
|
func NewManager(cfg *Config) *Manager {
|
||||||
|
return &Manager{
|
||||||
|
cfg: cfg,
|
||||||
|
activeReservations: make(map[ID]*FSM),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run runs the reservation manager.
|
||||||
|
func (m *Manager) Run(ctx context.Context, height int32) error {
|
||||||
|
// todo(sputn1ck): recover swaps on startup
|
||||||
|
log.Debugf("Starting reservation manager")
|
||||||
|
|
||||||
|
runCtx, cancel := context.WithCancel(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
currentHeight := height
|
||||||
|
|
||||||
|
err := m.RecoverReservations(runCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newBlockChan, newBlockErrChan, err := m.cfg.ChainNotifier.
|
||||||
|
RegisterBlockEpochNtfn(runCtx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reservationResChan := make(
|
||||||
|
chan *reservationrpc.ServerReservationNotification,
|
||||||
|
)
|
||||||
|
|
||||||
|
err = m.RegisterReservationNotifications(runCtx, reservationResChan)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case height := <-newBlockChan:
|
||||||
|
log.Debugf("Received block %v", height)
|
||||||
|
currentHeight = height
|
||||||
|
|
||||||
|
case reservationRes := <-reservationResChan:
|
||||||
|
log.Debugf("Received reservation %x",
|
||||||
|
reservationRes.ReservationId)
|
||||||
|
_, err := m.newReservation(
|
||||||
|
runCtx, uint32(currentHeight), reservationRes,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
case err := <-newBlockErrChan:
|
||||||
|
return err
|
||||||
|
|
||||||
|
case <-runCtx.Done():
|
||||||
|
log.Debugf("Stopping reservation manager")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newReservation creates a new reservation from the reservation request.
|
||||||
|
func (m *Manager) newReservation(ctx context.Context, currentHeight uint32,
|
||||||
|
req *reservationrpc.ServerReservationNotification) (*FSM, error) {
|
||||||
|
|
||||||
|
var reservationID ID
|
||||||
|
err := reservationID.FromByteSlice(
|
||||||
|
req.ReservationId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverKey, err := btcec.ParsePubKey(req.ServerKey)
|
||||||
|
if err != nil {
|
||||||
|
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(
|
||||||
|
ctx, m.cfg,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add the reservation to the active reservations map.
|
||||||
|
m.Lock()
|
||||||
|
m.activeReservations[reservationID] = reservationFSM
|
||||||
|
m.Unlock()
|
||||||
|
|
||||||
|
initContext := &InitReservationContext{
|
||||||
|
reservationID: reservationID,
|
||||||
|
serverPubkey: serverKey,
|
||||||
|
value: btcutil.Amount(req.Value),
|
||||||
|
expiry: req.Expiry,
|
||||||
|
heightHint: currentHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the init event to the state machine.
|
||||||
|
go func() {
|
||||||
|
err = reservationFSM.SendEvent(OnServerRequest, initContext)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error sending init event: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// We'll now wait for the reservation to be in the state where it is
|
||||||
|
// waiting to be confirmed.
|
||||||
|
err = reservationFSM.DefaultObserver.WaitForState(
|
||||||
|
ctx, 5*time.Second, WaitForConfirmation,
|
||||||
|
fsm.WithWaitForStateOption(time.Second),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
if reservationFSM.LastActionError != nil {
|
||||||
|
return nil, fmt.Errorf("error waiting for "+
|
||||||
|
"state: %v, last action error: %v",
|
||||||
|
err, reservationFSM.LastActionError)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reservationFSM, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterReservationNotifications registers a new reservation notification
|
||||||
|
// stream.
|
||||||
|
func (m *Manager) RegisterReservationNotifications(
|
||||||
|
ctx context.Context, reservationChan chan *reservationrpc.
|
||||||
|
ServerReservationNotification) error {
|
||||||
|
|
||||||
|
// In order to create a valid lsat we first are going to call
|
||||||
|
// the FetchL402 method.
|
||||||
|
err := m.cfg.FetchL402(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now subscribe to the reservation notifications.
|
||||||
|
reservationStream, err := m.cfg.ReservationClient.
|
||||||
|
ReservationNotificationStream(
|
||||||
|
ctx, &reservationrpc.ReservationNotificationRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now start a goroutine that will forward all the reservation
|
||||||
|
// notifications to the reservationChan.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
reservationRes, err := reservationStream.Recv()
|
||||||
|
if err == nil && reservationRes != nil {
|
||||||
|
log.Debugf("Received reservation %x",
|
||||||
|
reservationRes.ReservationId)
|
||||||
|
reservationChan <- reservationRes
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Errorf("Error receiving "+
|
||||||
|
"reservation: %v", err)
|
||||||
|
|
||||||
|
reconnectTimer := time.NewTimer(time.Second * 10)
|
||||||
|
|
||||||
|
// If we encounter an error, we'll
|
||||||
|
// try to reconnect.
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-reconnectTimer.C:
|
||||||
|
err = m.RegisterReservationNotifications(
|
||||||
|
ctx, reservationChan,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
log.Debugf(
|
||||||
|
"Successfully " +
|
||||||
|
"reconnected",
|
||||||
|
)
|
||||||
|
reconnectTimer.Stop()
|
||||||
|
// If we were able to
|
||||||
|
// reconnect, we'll
|
||||||
|
// return.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Errorf("Error "+
|
||||||
|
"reconnecting: %v",
|
||||||
|
err)
|
||||||
|
|
||||||
|
reconnectTimer.Reset(
|
||||||
|
time.Second * 10,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecoverReservations tries to recover all reservations that are still active
|
||||||
|
// from the database.
|
||||||
|
func (m *Manager) RecoverReservations(ctx context.Context) error {
|
||||||
|
reservations, err := m.cfg.Store.ListReservations(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, reservation := range reservations {
|
||||||
|
if isFinalState(reservation.State) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("Recovering reservation %x", reservation.ID)
|
||||||
|
|
||||||
|
fsmCtx := context.WithValue(ctx, reservation.ID, nil)
|
||||||
|
|
||||||
|
reservationFSM := NewFSMFromReservation(
|
||||||
|
fsmCtx, m.cfg, reservation,
|
||||||
|
)
|
||||||
|
|
||||||
|
m.activeReservations[reservation.ID] = reservationFSM
|
||||||
|
|
||||||
|
// As SendEvent can block, we'll start a goroutine to process
|
||||||
|
// the event.
|
||||||
|
go func() {
|
||||||
|
err := reservationFSM.SendEvent(OnRecover, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("FSM %v Error sending recover "+
|
||||||
|
"event %v, state: %v",
|
||||||
|
reservationFSM.reservation.ID, err,
|
||||||
|
reservationFSM.reservation.State)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReservations retrieves all reservations from the database.
|
||||||
|
func (m *Manager) GetReservations(ctx context.Context) ([]*Reservation, error) {
|
||||||
|
return m.cfg.Store.ListReservations(ctx)
|
||||||
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/loop/loopdb"
|
||||||
|
"github.com/lightninglabs/loop/swapserverrpc"
|
||||||
|
"github.com/lightninglabs/loop/test"
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
defaultReservationId = mustDecodeID("17cecc61ab4aafebdc0542dabdae0d0cb8907ec1c9c8ae387bc5a3309ca8b600")
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestManager(t *testing.T) {
|
||||||
|
ctxb, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
testContext := newManagerTestContext(t)
|
||||||
|
|
||||||
|
// Start the manager.
|
||||||
|
go func() {
|
||||||
|
err := testContext.manager.Run(ctxb, testContext.mockLnd.Height)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create a new reservation.
|
||||||
|
fsm, err := testContext.manager.newReservation(
|
||||||
|
ctxb, uint32(testContext.mockLnd.Height),
|
||||||
|
&swapserverrpc.ServerReservationNotification{
|
||||||
|
ReservationId: defaultReservationId[:],
|
||||||
|
Value: uint64(defaultValue),
|
||||||
|
ServerKey: defaultPubkeyBytes,
|
||||||
|
Expiry: uint32(testContext.mockLnd.Height) + defaultExpiry,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// We'll expect the spendConfirmation to be sent to the server.
|
||||||
|
pkScript, err := fsm.reservation.GetPkScript()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
conf := <-testContext.mockLnd.RegisterConfChannel
|
||||||
|
require.Equal(t, conf.PkScript, pkScript)
|
||||||
|
|
||||||
|
confTx := &wire.MsgTx{
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
PkScript: pkScript,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// We'll now confirm the spend.
|
||||||
|
conf.ConfChan <- &chainntnfs.TxConfirmation{
|
||||||
|
BlockHeight: uint32(testContext.mockLnd.Height),
|
||||||
|
Tx: confTx,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now expect the reservation to be confirmed.
|
||||||
|
err = fsm.DefaultObserver.WaitForState(ctxb, 5*time.Second, Confirmed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// We'll now expire the reservation.
|
||||||
|
err = testContext.mockLnd.NotifyHeight(
|
||||||
|
testContext.mockLnd.Height + int32(defaultExpiry),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// We'll now expect the reservation to be expired.
|
||||||
|
err = fsm.DefaultObserver.WaitForState(ctxb, 5*time.Second, TimedOut)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ManagerTestContext is a helper struct that contains all the necessary
|
||||||
|
// components to test the reservation manager.
|
||||||
|
type ManagerTestContext struct {
|
||||||
|
manager *Manager
|
||||||
|
context test.Context
|
||||||
|
mockLnd *test.LndMockServices
|
||||||
|
reservationNotificationChan chan *swapserverrpc.ServerReservationNotification
|
||||||
|
mockReservationClient *mockReservationClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// newManagerTestContext creates a new test context for the reservation manager.
|
||||||
|
func newManagerTestContext(t *testing.T) *ManagerTestContext {
|
||||||
|
mockLnd := test.NewMockLnd()
|
||||||
|
lndContext := test.NewContext(t, mockLnd)
|
||||||
|
|
||||||
|
dbFixture := loopdb.NewTestDB(t)
|
||||||
|
|
||||||
|
store := NewSQLStore(dbFixture)
|
||||||
|
|
||||||
|
mockReservationClient := new(mockReservationClient)
|
||||||
|
|
||||||
|
sendChan := make(chan *swapserverrpc.ServerReservationNotification)
|
||||||
|
|
||||||
|
mockReservationClient.On(
|
||||||
|
"ReservationNotificationStream", mock.Anything, mock.Anything,
|
||||||
|
mock.Anything,
|
||||||
|
).Return(
|
||||||
|
&dummyReservationNotificationServer{
|
||||||
|
SendChan: sendChan,
|
||||||
|
}, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
mockReservationClient.On(
|
||||||
|
"OpenReservation", mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(
|
||||||
|
&swapserverrpc.ServerOpenReservationResponse{}, nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
Store: store,
|
||||||
|
Wallet: mockLnd.WalletKit,
|
||||||
|
ChainNotifier: mockLnd.ChainNotifier,
|
||||||
|
FetchL402: func(context.Context) error { return nil },
|
||||||
|
ReservationClient: mockReservationClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := NewManager(cfg)
|
||||||
|
|
||||||
|
return &ManagerTestContext{
|
||||||
|
manager: manager,
|
||||||
|
context: lndContext,
|
||||||
|
mockLnd: mockLnd,
|
||||||
|
mockReservationClient: mockReservationClient,
|
||||||
|
reservationNotificationChan: sendChan,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyReservationNotificationServer struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
|
||||||
|
// SendChan is the channel that is used to send notifications.
|
||||||
|
SendChan chan *swapserverrpc.ServerReservationNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dummyReservationNotificationServer) Recv() (
|
||||||
|
*swapserverrpc.ServerReservationNotification, error) {
|
||||||
|
|
||||||
|
return <-d.SendChan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustDecodeID(id string) ID {
|
||||||
|
bytes, err := hex.DecodeString(id)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
var decoded ID
|
||||||
|
copy(decoded[:], bytes)
|
||||||
|
return decoded
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
reservation_script "github.com/lightninglabs/loop/instantout/reservation/script"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ID is a unique identifier for a reservation.
|
||||||
|
type ID [IdLength]byte
|
||||||
|
|
||||||
|
// FromByteSlice creates a reservation id from a byte slice.
|
||||||
|
func (r *ID) FromByteSlice(b []byte) error {
|
||||||
|
if len(b) != IdLength {
|
||||||
|
return fmt.Errorf("reservation id must be 32 bytes, got %d, %x",
|
||||||
|
len(b), b)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(r[:], b)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reservation holds all the necessary information for the 2-of-2 multisig
|
||||||
|
// reservation utxo.
|
||||||
|
type Reservation struct {
|
||||||
|
// ID is the unique identifier of the reservation.
|
||||||
|
ID ID
|
||||||
|
|
||||||
|
// State is the current state of the reservation.
|
||||||
|
State fsm.StateType
|
||||||
|
|
||||||
|
// ClientPubkey is the client's pubkey.
|
||||||
|
ClientPubkey *btcec.PublicKey
|
||||||
|
|
||||||
|
// ServerPubkey is the server's pubkey.
|
||||||
|
ServerPubkey *btcec.PublicKey
|
||||||
|
|
||||||
|
// Value is the amount of the reservation.
|
||||||
|
Value btcutil.Amount
|
||||||
|
|
||||||
|
// Expiry is the absolute block height at which the reservation expires.
|
||||||
|
Expiry uint32
|
||||||
|
|
||||||
|
// KeyLocator is the key locator of the client's key.
|
||||||
|
KeyLocator keychain.KeyLocator
|
||||||
|
|
||||||
|
// Outpoint is the outpoint of the reservation.
|
||||||
|
Outpoint *wire.OutPoint
|
||||||
|
|
||||||
|
// InitiationHeight is the height at which the reservation was
|
||||||
|
// initiated.
|
||||||
|
InitiationHeight int32
|
||||||
|
|
||||||
|
// ConfirmationHeight is the height at which the reservation was
|
||||||
|
// confirmed.
|
||||||
|
ConfirmationHeight uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReservation(id ID, serverPubkey, clientPubkey *btcec.PublicKey,
|
||||||
|
value btcutil.Amount, expiry, heightHint uint32,
|
||||||
|
keyLocator keychain.KeyLocator) (*Reservation,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
if id == [32]byte{} {
|
||||||
|
return nil, errors.New("id is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clientPubkey == nil {
|
||||||
|
return nil, errors.New("client pubkey is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if serverPubkey == nil {
|
||||||
|
return nil, errors.New("server pubkey is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if expiry == 0 {
|
||||||
|
return nil, errors.New("expiry is 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value == 0 {
|
||||||
|
return nil, errors.New("value is 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyLocator.Family == 0 {
|
||||||
|
return nil, errors.New("key locator family is 0")
|
||||||
|
}
|
||||||
|
return &Reservation{
|
||||||
|
ID: id,
|
||||||
|
Value: value,
|
||||||
|
ClientPubkey: clientPubkey,
|
||||||
|
ServerPubkey: serverPubkey,
|
||||||
|
KeyLocator: keyLocator,
|
||||||
|
Expiry: expiry,
|
||||||
|
InitiationHeight: int32(heightHint),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPkScript returns the pk script of the reservation.
|
||||||
|
func (r *Reservation) GetPkScript() ([]byte, error) {
|
||||||
|
// Now that we have all the required data, we can create the pk script.
|
||||||
|
pkScript, err := reservation_script.ReservationScript(
|
||||||
|
r.Expiry, r.ServerPubkey, r.ClientPubkey,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkScript, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reservation) findReservationOutput(tx *wire.MsgTx) (*wire.OutPoint,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
pkScript, err := r.GetPkScript()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, txOut := range tx.TxOut {
|
||||||
|
if bytes.Equal(txOut.PkScript, pkScript) {
|
||||||
|
return &wire.OutPoint{
|
||||||
|
Hash: tx.TxHash(),
|
||||||
|
Index: uint32(i),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("reservation output not found")
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Init: OnServerRequest
|
||||||
|
Confirmed
|
||||||
|
Confirmed --> TimedOut: OnTimedOut
|
||||||
|
Confirmed --> Confirmed: OnRecover
|
||||||
|
Failed
|
||||||
|
Init
|
||||||
|
Init --> Failed: OnError
|
||||||
|
Init --> WaitForConfirmation: OnBroadcast
|
||||||
|
Init --> Failed: OnRecover
|
||||||
|
TimedOut
|
||||||
|
WaitForConfirmation
|
||||||
|
WaitForConfirmation --> WaitForConfirmation: OnRecover
|
||||||
|
WaitForConfirmation --> Confirmed: OnConfirmed
|
||||||
|
WaitForConfirmation --> TimedOut: OnTimedOut
|
||||||
|
```
|
@ -0,0 +1,298 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
"github.com/lightninglabs/loop/loopdb"
|
||||||
|
"github.com/lightninglabs/loop/loopdb/sqlc"
|
||||||
|
"github.com/lightningnetwork/lnd/clock"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BaseDB is the interface that contains all the queries generated
|
||||||
|
// by sqlc for the reservation table.
|
||||||
|
type BaseDB interface {
|
||||||
|
// CreateReservation stores the reservation in the database.
|
||||||
|
CreateReservation(ctx context.Context,
|
||||||
|
arg sqlc.CreateReservationParams) error
|
||||||
|
|
||||||
|
// GetReservation retrieves the reservation from the database.
|
||||||
|
GetReservation(ctx context.Context,
|
||||||
|
reservationID []byte) (sqlc.Reservation, error)
|
||||||
|
|
||||||
|
// GetReservationUpdates fetches all updates for a reservation.
|
||||||
|
GetReservationUpdates(ctx context.Context,
|
||||||
|
reservationID []byte) ([]sqlc.ReservationUpdate, error)
|
||||||
|
|
||||||
|
// GetReservations lists all existing reservations the client has ever
|
||||||
|
// made.
|
||||||
|
GetReservations(ctx context.Context) ([]sqlc.Reservation, error)
|
||||||
|
|
||||||
|
// UpdateReservation inserts a new reservation update.
|
||||||
|
UpdateReservation(ctx context.Context,
|
||||||
|
arg sqlc.UpdateReservationParams) error
|
||||||
|
|
||||||
|
// ExecTx allows for executing a function in the context of a database
|
||||||
|
// transaction.
|
||||||
|
ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
|
||||||
|
txBody func(*sqlc.Queries) error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLStore manages the reservations in the database.
|
||||||
|
type SQLStore struct {
|
||||||
|
baseDb BaseDB
|
||||||
|
|
||||||
|
clock clock.Clock
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSQLStore creates a new SQLStore.
|
||||||
|
func NewSQLStore(db BaseDB) *SQLStore {
|
||||||
|
return &SQLStore{
|
||||||
|
baseDb: db,
|
||||||
|
clock: clock.NewDefaultClock(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateReservation stores the reservation in the database.
|
||||||
|
func (r *SQLStore) CreateReservation(ctx context.Context,
|
||||||
|
reservation *Reservation) error {
|
||||||
|
|
||||||
|
args := sqlc.CreateReservationParams{
|
||||||
|
ReservationID: reservation.ID[:],
|
||||||
|
ClientPubkey: reservation.ClientPubkey.SerializeCompressed(),
|
||||||
|
ServerPubkey: reservation.ServerPubkey.SerializeCompressed(),
|
||||||
|
Expiry: int32(reservation.Expiry),
|
||||||
|
Value: int64(reservation.Value),
|
||||||
|
ClientKeyFamily: int32(reservation.KeyLocator.Family),
|
||||||
|
ClientKeyIndex: int32(reservation.KeyLocator.Index),
|
||||||
|
InitiationHeight: reservation.InitiationHeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateArgs := sqlc.InsertReservationUpdateParams{
|
||||||
|
ReservationID: reservation.ID[:],
|
||||||
|
UpdateTimestamp: r.clock.Now().UTC(),
|
||||||
|
UpdateState: string(reservation.State),
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.baseDb.ExecTx(ctx, &loopdb.SqliteTxOptions{},
|
||||||
|
func(q *sqlc.Queries) error {
|
||||||
|
err := q.CreateReservation(ctx, args)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.InsertReservationUpdate(ctx, updateArgs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateReservation updates the reservation in the database.
|
||||||
|
func (r *SQLStore) UpdateReservation(ctx context.Context,
|
||||||
|
reservation *Reservation) error {
|
||||||
|
|
||||||
|
var txHash []byte
|
||||||
|
var outIndex sql.NullInt32
|
||||||
|
if reservation.Outpoint != nil {
|
||||||
|
txHash = reservation.Outpoint.Hash[:]
|
||||||
|
outIndex = sql.NullInt32{
|
||||||
|
Int32: int32(reservation.Outpoint.Index),
|
||||||
|
Valid: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insertUpdateArgs := sqlc.InsertReservationUpdateParams{
|
||||||
|
ReservationID: reservation.ID[:],
|
||||||
|
UpdateTimestamp: r.clock.Now().UTC(),
|
||||||
|
UpdateState: string(reservation.State),
|
||||||
|
}
|
||||||
|
|
||||||
|
updateArgs := sqlc.UpdateReservationParams{
|
||||||
|
ReservationID: reservation.ID[:],
|
||||||
|
TxHash: txHash,
|
||||||
|
OutIndex: outIndex,
|
||||||
|
ConfirmationHeight: marshalSqlNullInt32(
|
||||||
|
int32(reservation.ConfirmationHeight),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.baseDb.ExecTx(ctx, &loopdb.SqliteTxOptions{},
|
||||||
|
func(q *sqlc.Queries) error {
|
||||||
|
err := q.UpdateReservation(ctx, updateArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return q.InsertReservationUpdate(ctx, insertUpdateArgs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetReservation retrieves the reservation from the database.
|
||||||
|
func (r *SQLStore) GetReservation(ctx context.Context,
|
||||||
|
reservationId ID) (*Reservation, error) {
|
||||||
|
|
||||||
|
var reservation *Reservation
|
||||||
|
err := r.baseDb.ExecTx(ctx, loopdb.NewSqlReadOpts(),
|
||||||
|
func(q *sqlc.Queries) error {
|
||||||
|
var err error
|
||||||
|
reservationRow, err := q.GetReservation(
|
||||||
|
ctx, reservationId[:],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reservationUpdates, err := q.GetReservationUpdates(
|
||||||
|
ctx, reservationId[:],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reservationUpdates) == 0 {
|
||||||
|
return errors.New("no reservation updates")
|
||||||
|
}
|
||||||
|
|
||||||
|
reservation, err = sqlReservationToReservation(
|
||||||
|
reservationRow,
|
||||||
|
reservationUpdates[len(reservationUpdates)-1],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return reservation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListReservations lists all existing reservations the client has ever made.
|
||||||
|
func (r *SQLStore) ListReservations(ctx context.Context) ([]*Reservation,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
var result []*Reservation
|
||||||
|
|
||||||
|
err := r.baseDb.ExecTx(ctx, loopdb.NewSqlReadOpts(),
|
||||||
|
func(q *sqlc.Queries) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
reservations, err := q.GetReservations(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, reservation := range reservations {
|
||||||
|
reservationUpdates, err := q.GetReservationUpdates(
|
||||||
|
ctx, reservation.ReservationID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reservationUpdates) == 0 {
|
||||||
|
return errors.New(
|
||||||
|
"no reservation updates",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := sqlReservationToReservation(
|
||||||
|
reservation, reservationUpdates[len(
|
||||||
|
reservationUpdates,
|
||||||
|
)-1],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqlReservationToReservation converts a sql reservation to a reservation.
|
||||||
|
func sqlReservationToReservation(row sqlc.Reservation,
|
||||||
|
lastUpdate sqlc.ReservationUpdate) (*Reservation,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
id := ID{}
|
||||||
|
err := id.FromByteSlice(row.ReservationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientPubkey, err := btcec.ParsePubKey(row.ClientPubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serverPubkey, err := btcec.ParsePubKey(row.ServerPubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var txHash *chainhash.Hash
|
||||||
|
if row.TxHash != nil {
|
||||||
|
txHash, err = chainhash.NewHash(row.TxHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var outpoint *wire.OutPoint
|
||||||
|
if row.OutIndex.Valid {
|
||||||
|
outpoint = wire.NewOutPoint(
|
||||||
|
txHash, uint32(unmarshalSqlNullInt32(row.OutIndex)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Reservation{
|
||||||
|
ID: id,
|
||||||
|
ClientPubkey: clientPubkey,
|
||||||
|
ServerPubkey: serverPubkey,
|
||||||
|
Expiry: uint32(row.Expiry),
|
||||||
|
Value: btcutil.Amount(row.Value),
|
||||||
|
KeyLocator: keychain.KeyLocator{
|
||||||
|
Family: keychain.KeyFamily(row.ClientKeyFamily),
|
||||||
|
Index: uint32(row.ClientKeyIndex),
|
||||||
|
},
|
||||||
|
Outpoint: outpoint,
|
||||||
|
ConfirmationHeight: uint32(
|
||||||
|
unmarshalSqlNullInt32(row.ConfirmationHeight),
|
||||||
|
),
|
||||||
|
InitiationHeight: row.InitiationHeight,
|
||||||
|
State: fsm.StateType(lastUpdate.UpdateState),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// marshalSqlNullInt32 converts an int32 to a sql.NullInt32.
|
||||||
|
func marshalSqlNullInt32(i int32) sql.NullInt32 {
|
||||||
|
return sql.NullInt32{
|
||||||
|
Int32: i,
|
||||||
|
Valid: i != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unmarshalSqlNullInt32 converts a sql.NullInt32 to an int32.
|
||||||
|
func unmarshalSqlNullInt32(i sql.NullInt32) int32 {
|
||||||
|
if i.Valid {
|
||||||
|
return i.Int32
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
@ -0,0 +1,96 @@
|
|||||||
|
package reservation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightninglabs/loop/fsm"
|
||||||
|
"github.com/lightninglabs/loop/loopdb"
|
||||||
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSqlStore tests the basic functionality of the SQLStore.
|
||||||
|
func TestSqlStore(t *testing.T) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
testDb := loopdb.NewTestDB(t)
|
||||||
|
defer testDb.Close()
|
||||||
|
|
||||||
|
store := NewSQLStore(testDb)
|
||||||
|
|
||||||
|
// Create a reservation and store it.
|
||||||
|
reservation := &Reservation{
|
||||||
|
ID: getRandomReservationID(),
|
||||||
|
State: fsm.StateType("init"),
|
||||||
|
ClientPubkey: defaultPubkey,
|
||||||
|
ServerPubkey: defaultPubkey,
|
||||||
|
Value: 100,
|
||||||
|
Expiry: 100,
|
||||||
|
KeyLocator: keychain.KeyLocator{
|
||||||
|
Family: 1,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := store.CreateReservation(ctxb, reservation)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the reservation and compare it.
|
||||||
|
reservation2, err := store.GetReservation(ctxb, reservation.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, reservation, reservation2)
|
||||||
|
|
||||||
|
// Update the reservation and compare it.
|
||||||
|
reservation.State = fsm.StateType("state2")
|
||||||
|
err = store.UpdateReservation(ctxb, reservation)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
reservation2, err = store.GetReservation(ctxb, reservation.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, reservation, reservation2)
|
||||||
|
|
||||||
|
// Add an outpoint to the reservation and compare it.
|
||||||
|
reservation.Outpoint = &wire.OutPoint{
|
||||||
|
Hash: chainhash.Hash{0x01},
|
||||||
|
Index: 0,
|
||||||
|
}
|
||||||
|
reservation.State = Confirmed
|
||||||
|
|
||||||
|
err = store.UpdateReservation(ctxb, reservation)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
reservation2, err = store.GetReservation(ctxb, reservation.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, reservation, reservation2)
|
||||||
|
|
||||||
|
// Add a second reservation.
|
||||||
|
reservation3 := &Reservation{
|
||||||
|
ID: getRandomReservationID(),
|
||||||
|
State: fsm.StateType("init"),
|
||||||
|
ClientPubkey: defaultPubkey,
|
||||||
|
ServerPubkey: defaultPubkey,
|
||||||
|
Value: 99,
|
||||||
|
Expiry: 100,
|
||||||
|
KeyLocator: keychain.KeyLocator{
|
||||||
|
Family: 1,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.CreateReservation(ctxb, reservation3)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
reservations, err := store.ListReservations(ctxb)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 2, len(reservations))
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRandomReservationID generates a random reservation ID.
|
||||||
|
func getRandomReservationID() ID {
|
||||||
|
var id ID
|
||||||
|
rand.Read(id[:]) // nolint: errcheck
|
||||||
|
return id
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
DROP TABLE IF EXISTS reservation_updates;
|
||||||
|
DROP TABLE IF EXISTS reservations;
|
@ -0,0 +1,56 @@
|
|||||||
|
-- reservations contains all the information about a reservation.
|
||||||
|
CREATE TABLE IF NOT EXISTS reservations (
|
||||||
|
-- id is the auto incrementing primary key.
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
|
||||||
|
-- reservation_id is the unique identifier for the reservation.
|
||||||
|
reservation_id BLOB NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
-- client_pubkey is the public key of the client.
|
||||||
|
client_pubkey BLOB NOT NULL,
|
||||||
|
|
||||||
|
-- server_pubkey is the public key of the server.
|
||||||
|
server_pubkey BLOB NOT NULL,
|
||||||
|
|
||||||
|
-- expiry is the absolute expiry height of the reservation.
|
||||||
|
expiry INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- value is the value of the reservation.
|
||||||
|
value BIGINT NOT NULL,
|
||||||
|
|
||||||
|
-- client_key_family is the key family of the client.
|
||||||
|
client_key_family INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- client_key_index is the key index of the client.
|
||||||
|
client_key_index INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- initiation_height is the height at which the reservation was initiated.
|
||||||
|
initiation_height INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- tx_hash is the hash of the transaction that created the reservation.
|
||||||
|
tx_hash BLOB,
|
||||||
|
|
||||||
|
-- out_index is the index of the output that created the reservation.
|
||||||
|
out_index INTEGER,
|
||||||
|
|
||||||
|
-- confirmation_height is the height at which the reservation was confirmed.
|
||||||
|
confirmation_height INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS reservations_reservation_id_idx ON reservations(reservation_id);
|
||||||
|
|
||||||
|
-- reservation_updates contains all the updates to a reservation.
|
||||||
|
CREATE TABLE IF NOT EXISTS reservation_updates (
|
||||||
|
-- id is the auto incrementing primary key.
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
|
||||||
|
-- reservation_id is the unique identifier for the reservation.
|
||||||
|
reservation_id BLOB NOT NULL REFERENCES reservations(reservation_id),
|
||||||
|
|
||||||
|
-- update_state is the state of the reservation at the time of the update.
|
||||||
|
update_state TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- update_timestamp is the timestamp of the update.
|
||||||
|
update_timestamp TIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,66 @@
|
|||||||
|
-- name: CreateReservation :exec
|
||||||
|
INSERT INTO reservations (
|
||||||
|
reservation_id,
|
||||||
|
client_pubkey,
|
||||||
|
server_pubkey,
|
||||||
|
expiry,
|
||||||
|
value,
|
||||||
|
client_key_family,
|
||||||
|
client_key_index,
|
||||||
|
initiation_height
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
$7,
|
||||||
|
$8
|
||||||
|
);
|
||||||
|
|
||||||
|
-- name: UpdateReservation :exec
|
||||||
|
UPDATE reservations
|
||||||
|
SET
|
||||||
|
tx_hash = $2,
|
||||||
|
out_index = $3,
|
||||||
|
confirmation_height = $4
|
||||||
|
WHERE
|
||||||
|
reservations.reservation_id = $1;
|
||||||
|
|
||||||
|
-- name: InsertReservationUpdate :exec
|
||||||
|
INSERT INTO reservation_updates (
|
||||||
|
reservation_id,
|
||||||
|
update_state,
|
||||||
|
update_timestamp
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3
|
||||||
|
);
|
||||||
|
|
||||||
|
-- name: GetReservation :one
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
reservations
|
||||||
|
WHERE
|
||||||
|
reservation_id = $1;
|
||||||
|
|
||||||
|
-- name: GetReservations :many
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
reservations
|
||||||
|
ORDER BY
|
||||||
|
id ASC;
|
||||||
|
|
||||||
|
-- name: GetReservationUpdates :many
|
||||||
|
SELECT
|
||||||
|
reservation_updates.*
|
||||||
|
FROM
|
||||||
|
reservation_updates
|
||||||
|
WHERE
|
||||||
|
reservation_id = $1
|
||||||
|
ORDER BY
|
||||||
|
id ASC;
|
@ -0,0 +1,222 @@
|
|||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.17.2
|
||||||
|
// source: reservations.sql
|
||||||
|
|
||||||
|
package sqlc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const createReservation = `-- name: CreateReservation :exec
|
||||||
|
INSERT INTO reservations (
|
||||||
|
reservation_id,
|
||||||
|
client_pubkey,
|
||||||
|
server_pubkey,
|
||||||
|
expiry,
|
||||||
|
value,
|
||||||
|
client_key_family,
|
||||||
|
client_key_index,
|
||||||
|
initiation_height
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3,
|
||||||
|
$4,
|
||||||
|
$5,
|
||||||
|
$6,
|
||||||
|
$7,
|
||||||
|
$8
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type CreateReservationParams struct {
|
||||||
|
ReservationID []byte
|
||||||
|
ClientPubkey []byte
|
||||||
|
ServerPubkey []byte
|
||||||
|
Expiry int32
|
||||||
|
Value int64
|
||||||
|
ClientKeyFamily int32
|
||||||
|
ClientKeyIndex int32
|
||||||
|
InitiationHeight int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) CreateReservation(ctx context.Context, arg CreateReservationParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, createReservation,
|
||||||
|
arg.ReservationID,
|
||||||
|
arg.ClientPubkey,
|
||||||
|
arg.ServerPubkey,
|
||||||
|
arg.Expiry,
|
||||||
|
arg.Value,
|
||||||
|
arg.ClientKeyFamily,
|
||||||
|
arg.ClientKeyIndex,
|
||||||
|
arg.InitiationHeight,
|
||||||
|
)
|
||||||
|
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
|
||||||
|
FROM
|
||||||
|
reservations
|
||||||
|
WHERE
|
||||||
|
reservation_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetReservation(ctx context.Context, reservationID []byte) (Reservation, error) {
|
||||||
|
row := q.db.QueryRowContext(ctx, getReservation, reservationID)
|
||||||
|
var i Reservation
|
||||||
|
err := row.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ReservationID,
|
||||||
|
&i.ClientPubkey,
|
||||||
|
&i.ServerPubkey,
|
||||||
|
&i.Expiry,
|
||||||
|
&i.Value,
|
||||||
|
&i.ClientKeyFamily,
|
||||||
|
&i.ClientKeyIndex,
|
||||||
|
&i.InitiationHeight,
|
||||||
|
&i.TxHash,
|
||||||
|
&i.OutIndex,
|
||||||
|
&i.ConfirmationHeight,
|
||||||
|
)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReservationUpdates = `-- name: GetReservationUpdates :many
|
||||||
|
SELECT
|
||||||
|
reservation_updates.id, reservation_updates.reservation_id, reservation_updates.update_state, reservation_updates.update_timestamp
|
||||||
|
FROM
|
||||||
|
reservation_updates
|
||||||
|
WHERE
|
||||||
|
reservation_id = $1
|
||||||
|
ORDER BY
|
||||||
|
id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetReservationUpdates(ctx context.Context, reservationID []byte) ([]ReservationUpdate, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getReservationUpdates, reservationID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []ReservationUpdate
|
||||||
|
for rows.Next() {
|
||||||
|
var i ReservationUpdate
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ReservationID,
|
||||||
|
&i.UpdateState,
|
||||||
|
&i.UpdateTimestamp,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
FROM
|
||||||
|
reservations
|
||||||
|
ORDER BY
|
||||||
|
id ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetReservations(ctx context.Context) ([]Reservation, error) {
|
||||||
|
rows, err := q.db.QueryContext(ctx, getReservations)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []Reservation
|
||||||
|
for rows.Next() {
|
||||||
|
var i Reservation
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ReservationID,
|
||||||
|
&i.ClientPubkey,
|
||||||
|
&i.ServerPubkey,
|
||||||
|
&i.Expiry,
|
||||||
|
&i.Value,
|
||||||
|
&i.ClientKeyFamily,
|
||||||
|
&i.ClientKeyIndex,
|
||||||
|
&i.InitiationHeight,
|
||||||
|
&i.TxHash,
|
||||||
|
&i.OutIndex,
|
||||||
|
&i.ConfirmationHeight,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertReservationUpdate = `-- name: InsertReservationUpdate :exec
|
||||||
|
INSERT INTO reservation_updates (
|
||||||
|
reservation_id,
|
||||||
|
update_state,
|
||||||
|
update_timestamp
|
||||||
|
) VALUES (
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
$3
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertReservationUpdateParams struct {
|
||||||
|
ReservationID []byte
|
||||||
|
UpdateState string
|
||||||
|
UpdateTimestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertReservationUpdate(ctx context.Context, arg InsertReservationUpdateParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, insertReservationUpdate, arg.ReservationID, arg.UpdateState, arg.UpdateTimestamp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateReservation = `-- name: UpdateReservation :exec
|
||||||
|
UPDATE reservations
|
||||||
|
SET
|
||||||
|
tx_hash = $2,
|
||||||
|
out_index = $3,
|
||||||
|
confirmation_height = $4
|
||||||
|
WHERE
|
||||||
|
reservations.reservation_id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type UpdateReservationParams struct {
|
||||||
|
ReservationID []byte
|
||||||
|
TxHash []byte
|
||||||
|
OutIndex sql.NullInt32
|
||||||
|
ConfirmationHeight sql.NullInt32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) UpdateReservation(ctx context.Context, arg UpdateReservationParams) error {
|
||||||
|
_, err := q.db.ExecContext(ctx, updateReservation,
|
||||||
|
arg.ReservationID,
|
||||||
|
arg.TxHash,
|
||||||
|
arg.OutIndex,
|
||||||
|
arg.ConfirmationHeight,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
@ -1,2 +1,3 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
go run ./fsm/stateparser/stateparser.go --out ./fsm/example_fsm.md --fsm example
|
go run ./fsm/stateparser/stateparser.go --out ./fsm/example_fsm.md --fsm example
|
||||||
|
go run ./fsm/stateparser/stateparser.go --out ./reservation/reservation_fsm.md --fsm reservation
|
@ -0,0 +1,468 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.30.0
|
||||||
|
// protoc v3.6.1
|
||||||
|
// source: reservation.proto
|
||||||
|
|
||||||
|
// We can't change this to swapserverrpc, it would be a breaking change because
|
||||||
|
// the package name is also contained in the HTTP URIs and old clients would
|
||||||
|
// call the wrong endpoints. Luckily with the go_package option we can have
|
||||||
|
// different golang and RPC package names to fix protobuf namespace conflicts.
|
||||||
|
|
||||||
|
package swapserverrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReservationProtocolVersion is the version of the reservation protocol.
|
||||||
|
type ReservationProtocolVersion int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RESERVATION_NONE is the default value and means that the reservation
|
||||||
|
// protocol version is not set.
|
||||||
|
ReservationProtocolVersion_RESERVATION_NONE ReservationProtocolVersion = 0
|
||||||
|
// RESERVATION_SERVER_REQUEST is the first version of the reservation
|
||||||
|
// protocol where the server notifies the client about a reservation.
|
||||||
|
ReservationProtocolVersion_RESERVATION_SERVER_NOTIFY ReservationProtocolVersion = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enum value maps for ReservationProtocolVersion.
|
||||||
|
var (
|
||||||
|
ReservationProtocolVersion_name = map[int32]string{
|
||||||
|
0: "RESERVATION_NONE",
|
||||||
|
1: "RESERVATION_SERVER_NOTIFY",
|
||||||
|
}
|
||||||
|
ReservationProtocolVersion_value = map[string]int32{
|
||||||
|
"RESERVATION_NONE": 0,
|
||||||
|
"RESERVATION_SERVER_NOTIFY": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (x ReservationProtocolVersion) Enum() *ReservationProtocolVersion {
|
||||||
|
p := new(ReservationProtocolVersion)
|
||||||
|
*p = x
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ReservationProtocolVersion) String() string {
|
||||||
|
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ReservationProtocolVersion) Descriptor() protoreflect.EnumDescriptor {
|
||||||
|
return file_reservation_proto_enumTypes[0].Descriptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ReservationProtocolVersion) Type() protoreflect.EnumType {
|
||||||
|
return &file_reservation_proto_enumTypes[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x ReservationProtocolVersion) Number() protoreflect.EnumNumber {
|
||||||
|
return protoreflect.EnumNumber(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ReservationProtocolVersion.Descriptor instead.
|
||||||
|
func (ReservationProtocolVersion) EnumDescriptor() ([]byte, []int) {
|
||||||
|
return file_reservation_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReservationNotificationRequest is an empty request sent from the client to
|
||||||
|
// the server to open a stream to receive reservation notifications.
|
||||||
|
type ReservationNotificationRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ReservationNotificationRequest) Reset() {
|
||||||
|
*x = ReservationNotificationRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_reservation_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ReservationNotificationRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ReservationNotificationRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ReservationNotificationRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_reservation_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ReservationNotificationRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ReservationNotificationRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_reservation_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerReservationNotification is a notification sent from the server to the
|
||||||
|
// client if the server wants to open a reservation.
|
||||||
|
type ServerReservationNotification struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// reservation_id is the id of the reservation.
|
||||||
|
ReservationId []byte `protobuf:"bytes,1,opt,name=reservation_id,json=reservationId,proto3" json:"reservation_id,omitempty"`
|
||||||
|
// value is the value of the reservation in satoshis.
|
||||||
|
Value uint64 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"`
|
||||||
|
// server_key is the public key of the server.
|
||||||
|
ServerKey []byte `protobuf:"bytes,3,opt,name=server_key,json=serverKey,proto3" json:"server_key,omitempty"`
|
||||||
|
// expiry is the absolute expiry of the reservation.
|
||||||
|
Expiry uint32 `protobuf:"varint,4,opt,name=expiry,proto3" json:"expiry,omitempty"`
|
||||||
|
// protocol_version is the version of the reservation protocol.
|
||||||
|
ProtocolVersion ReservationProtocolVersion `protobuf:"varint,5,opt,name=protocol_version,json=protocolVersion,proto3,enum=looprpc.ReservationProtocolVersion" json:"protocol_version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerReservationNotification) Reset() {
|
||||||
|
*x = ServerReservationNotification{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_reservation_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerReservationNotification) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerReservationNotification) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ServerReservationNotification) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_reservation_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ServerReservationNotification.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ServerReservationNotification) Descriptor() ([]byte, []int) {
|
||||||
|
return file_reservation_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerReservationNotification) GetReservationId() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.ReservationId
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerReservationNotification) GetValue() uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Value
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerReservationNotification) GetServerKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.ServerKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerReservationNotification) GetExpiry() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Expiry
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerReservationNotification) GetProtocolVersion() ReservationProtocolVersion {
|
||||||
|
if x != nil {
|
||||||
|
return x.ProtocolVersion
|
||||||
|
}
|
||||||
|
return ReservationProtocolVersion_RESERVATION_NONE
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerOpenReservationRequest is a request sent from the client to the server
|
||||||
|
// to confirm a reservation opening.
|
||||||
|
type ServerOpenReservationRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// reservation_id is the id of the reservation.
|
||||||
|
ReservationId []byte `protobuf:"bytes,1,opt,name=reservation_id,json=reservationId,proto3" json:"reservation_id,omitempty"`
|
||||||
|
// client_key is the public key of the client.
|
||||||
|
ClientKey []byte `protobuf:"bytes,2,opt,name=client_key,json=clientKey,proto3" json:"client_key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerOpenReservationRequest) Reset() {
|
||||||
|
*x = ServerOpenReservationRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_reservation_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerOpenReservationRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerOpenReservationRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ServerOpenReservationRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_reservation_proto_msgTypes[2]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ServerOpenReservationRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ServerOpenReservationRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_reservation_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerOpenReservationRequest) GetReservationId() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.ReservationId
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerOpenReservationRequest) GetClientKey() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.ClientKey
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerOpenReservationResponse is a response sent from the server to the
|
||||||
|
// client to confirm a reservation opening.
|
||||||
|
type ServerOpenReservationResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerOpenReservationResponse) Reset() {
|
||||||
|
*x = ServerOpenReservationResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_reservation_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ServerOpenReservationResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ServerOpenReservationResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ServerOpenReservationResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_reservation_proto_msgTypes[3]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ServerOpenReservationResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ServerOpenReservationResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_reservation_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_reservation_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_reservation_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x11, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72,
|
||||||
|
0x6f, 0x74, 0x6f, 0x12, 0x07, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x22, 0x20, 0x0a, 0x1e,
|
||||||
|
0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66,
|
||||||
|
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xe3,
|
||||||
|
0x01, 0x0a, 0x1d, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
|
0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
|
||||||
|
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76,
|
||||||
|
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
|
||||||
|
0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1d, 0x0a,
|
||||||
|
0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||||
|
0x0c, 0x52, 0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06,
|
||||||
|
0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x65, 0x78,
|
||||||
|
0x70, 0x69, 0x72, 0x79, 0x12, 0x4e, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c,
|
||||||
|
0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x23,
|
||||||
|
0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73,
|
||||||
|
0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x56, 0x65, 0x72,
|
||||||
|
0x73, 0x69, 0x6f, 0x6e, 0x22, 0x64, 0x0a, 0x1c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4f, 0x70,
|
||||||
|
0x65, 0x6e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71,
|
||||||
|
0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74,
|
||||||
|
0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x72, 0x65,
|
||||||
|
0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63,
|
||||||
|
0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
|
||||||
|
0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x22, 0x1f, 0x0a, 0x1d, 0x53, 0x65,
|
||||||
|
0x72, 0x76, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74,
|
||||||
|
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x51, 0x0a, 0x1a, 0x52,
|
||||||
|
0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x63,
|
||||||
|
0x6f, 0x6c, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x10, 0x52, 0x45, 0x53,
|
||||||
|
0x45, 0x52, 0x56, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12,
|
||||||
|
0x1d, 0x0a, 0x19, 0x52, 0x45, 0x53, 0x45, 0x52, 0x56, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53,
|
||||||
|
0x45, 0x52, 0x56, 0x45, 0x52, 0x5f, 0x4e, 0x4f, 0x54, 0x49, 0x46, 0x59, 0x10, 0x01, 0x32, 0xea,
|
||||||
|
0x01, 0x0a, 0x12, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65,
|
||||||
|
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x72, 0x0a, 0x1d, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||||
|
0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x27, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63,
|
||||||
|
0x2e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x74, 0x69,
|
||||||
|
0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
|
||||||
|
0x26, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
|
||||||
|
0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4e, 0x6f, 0x74, 0x69, 0x66,
|
||||||
|
0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x30, 0x01, 0x12, 0x60, 0x0a, 0x0f, 0x4f, 0x70, 0x65,
|
||||||
|
0x6e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x2e, 0x6c,
|
||||||
|
0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4f, 0x70, 0x65,
|
||||||
|
0x6e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75,
|
||||||
|
0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x6c, 0x6f, 0x6f, 0x70, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65,
|
||||||
|
0x72, 0x76, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74,
|
||||||
|
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2d, 0x5a, 0x2b, 0x67,
|
||||||
|
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e,
|
||||||
|
0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6c, 0x6f, 0x6f, 0x70, 0x2f, 0x73, 0x77, 0x61,
|
||||||
|
0x70, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
|
||||||
|
0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_reservation_proto_rawDescOnce sync.Once
|
||||||
|
file_reservation_proto_rawDescData = file_reservation_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_reservation_proto_rawDescGZIP() []byte {
|
||||||
|
file_reservation_proto_rawDescOnce.Do(func() {
|
||||||
|
file_reservation_proto_rawDescData = protoimpl.X.CompressGZIP(file_reservation_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_reservation_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_reservation_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||||
|
var file_reservation_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||||
|
var file_reservation_proto_goTypes = []interface{}{
|
||||||
|
(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
|
||||||
|
}
|
||||||
|
var file_reservation_proto_depIdxs = []int32{
|
||||||
|
0, // 0: looprpc.ServerReservationNotification.protocol_version:type_name -> looprpc.ReservationProtocolVersion
|
||||||
|
1, // 1: looprpc.ReservationService.ReservationNotificationStream:input_type -> looprpc.ReservationNotificationRequest
|
||||||
|
3, // 2: looprpc.ReservationService.OpenReservation:input_type -> looprpc.ServerOpenReservationRequest
|
||||||
|
2, // 3: looprpc.ReservationService.ReservationNotificationStream:output_type -> looprpc.ServerReservationNotification
|
||||||
|
4, // 4: looprpc.ReservationService.OpenReservation:output_type -> looprpc.ServerOpenReservationResponse
|
||||||
|
3, // [3:5] is the sub-list for method output_type
|
||||||
|
1, // [1:3] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_reservation_proto_init() }
|
||||||
|
func file_reservation_proto_init() {
|
||||||
|
if File_reservation_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_reservation_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*ReservationNotificationRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_reservation_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*ServerReservationNotification); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_reservation_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*ServerOpenReservationRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_reservation_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*ServerOpenReservationResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_reservation_proto_rawDesc,
|
||||||
|
NumEnums: 1,
|
||||||
|
NumMessages: 4,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_reservation_proto_goTypes,
|
||||||
|
DependencyIndexes: file_reservation_proto_depIdxs,
|
||||||
|
EnumInfos: file_reservation_proto_enumTypes,
|
||||||
|
MessageInfos: file_reservation_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_reservation_proto = out.File
|
||||||
|
file_reservation_proto_rawDesc = nil
|
||||||
|
file_reservation_proto_goTypes = nil
|
||||||
|
file_reservation_proto_depIdxs = nil
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
// We can't change this to swapserverrpc, it would be a breaking change because
|
||||||
|
// the package name is also contained in the HTTP URIs and old clients would
|
||||||
|
// call the wrong endpoints. Luckily with the go_package option we can have
|
||||||
|
// different golang and RPC package names to fix protobuf namespace conflicts.
|
||||||
|
package looprpc;
|
||||||
|
|
||||||
|
option go_package = "github.com/lightninglabs/loop/swapserverrpc";
|
||||||
|
|
||||||
|
service ReservationService {
|
||||||
|
// ReservationNotificationStream is a server side stream that sends
|
||||||
|
// notifications if the server wants to open a reservation to the client.
|
||||||
|
rpc ReservationNotificationStream (ReservationNotificationRequest)
|
||||||
|
returns (stream ServerReservationNotification);
|
||||||
|
|
||||||
|
// OpenReservation requests a new reservation UTXO from the server.
|
||||||
|
rpc OpenReservation (ServerOpenReservationRequest)
|
||||||
|
returns (ServerOpenReservationResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReservationNotificationRequest is an empty request sent from the client to
|
||||||
|
// the server to open a stream to receive reservation notifications.
|
||||||
|
message ReservationNotificationRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerReservationNotification is a notification sent from the server to the
|
||||||
|
// client if the server wants to open a reservation.
|
||||||
|
message ServerReservationNotification {
|
||||||
|
// reservation_id is the id of the reservation.
|
||||||
|
bytes reservation_id = 1;
|
||||||
|
|
||||||
|
// value is the value of the reservation in satoshis.
|
||||||
|
uint64 value = 2;
|
||||||
|
|
||||||
|
// server_key is the public key of the server.
|
||||||
|
bytes server_key = 3;
|
||||||
|
|
||||||
|
// expiry is the absolute expiry of the reservation.
|
||||||
|
uint32 expiry = 4;
|
||||||
|
|
||||||
|
// protocol_version is the version of the reservation protocol.
|
||||||
|
ReservationProtocolVersion protocol_version = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerOpenReservationRequest is a request sent from the client to the server
|
||||||
|
// to confirm a reservation opening.
|
||||||
|
message ServerOpenReservationRequest {
|
||||||
|
// reservation_id is the id of the reservation.
|
||||||
|
bytes reservation_id = 1;
|
||||||
|
|
||||||
|
// client_key is the public key of the client.
|
||||||
|
bytes client_key = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerOpenReservationResponse is a response sent from the server to the
|
||||||
|
// client to confirm a reservation opening.
|
||||||
|
message ServerOpenReservationResponse {
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReservationProtocolVersion is the version of the reservation protocol.
|
||||||
|
enum ReservationProtocolVersion {
|
||||||
|
// RESERVATION_NONE is the default value and means that the reservation
|
||||||
|
// protocol version is not set.
|
||||||
|
RESERVATION_NONE = 0;
|
||||||
|
|
||||||
|
// RESERVATION_SERVER_REQUEST is the first version of the reservation
|
||||||
|
// protocol where the server notifies the client about a reservation.
|
||||||
|
RESERVATION_SERVER_NOTIFY = 1;
|
||||||
|
};
|
@ -0,0 +1,171 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
|
||||||
|
package swapserverrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.32.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
// ReservationServiceClient is the client API for ReservationService service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type ReservationServiceClient interface {
|
||||||
|
// ReservationNotificationStream is a server side stream that sends
|
||||||
|
// notifications if the server wants to open a reservation to the client.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
type reservationServiceClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReservationServiceClient(cc grpc.ClientConnInterface) ReservationServiceClient {
|
||||||
|
return &reservationServiceClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *reservationServiceClient) ReservationNotificationStream(ctx context.Context, in *ReservationNotificationRequest, opts ...grpc.CallOption) (ReservationService_ReservationNotificationStreamClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &ReservationService_ServiceDesc.Streams[0], "/looprpc.ReservationService/ReservationNotificationStream", opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &reservationServiceReservationNotificationStreamClient{stream}
|
||||||
|
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := x.ClientStream.CloseSend(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReservationService_ReservationNotificationStreamClient interface {
|
||||||
|
Recv() (*ServerReservationNotification, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type reservationServiceReservationNotificationStreamClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *reservationServiceReservationNotificationStreamClient) Recv() (*ServerReservationNotification, error) {
|
||||||
|
m := new(ServerReservationNotification)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *reservationServiceClient) OpenReservation(ctx context.Context, in *ServerOpenReservationRequest, opts ...grpc.CallOption) (*ServerOpenReservationResponse, error) {
|
||||||
|
out := new(ServerOpenReservationResponse)
|
||||||
|
err := c.cc.Invoke(ctx, "/looprpc.ReservationService/OpenReservation", 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
|
||||||
|
type ReservationServiceServer interface {
|
||||||
|
// ReservationNotificationStream is a server side stream that sends
|
||||||
|
// notifications if the server wants to open a reservation to the client.
|
||||||
|
ReservationNotificationStream(*ReservationNotificationRequest, ReservationService_ReservationNotificationStreamServer) error
|
||||||
|
// OpenReservation requests a new reservation UTXO from the server.
|
||||||
|
OpenReservation(context.Context, *ServerOpenReservationRequest) (*ServerOpenReservationResponse, error)
|
||||||
|
mustEmbedUnimplementedReservationServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedReservationServiceServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedReservationServiceServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedReservationServiceServer) ReservationNotificationStream(*ReservationNotificationRequest, ReservationService_ReservationNotificationStreamServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method ReservationNotificationStream not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedReservationServiceServer) OpenReservation(context.Context, *ServerOpenReservationRequest) (*ServerOpenReservationResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method OpenReservation not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedReservationServiceServer) mustEmbedUnimplementedReservationServiceServer() {}
|
||||||
|
|
||||||
|
// UnsafeReservationServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to ReservationServiceServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeReservationServiceServer interface {
|
||||||
|
mustEmbedUnimplementedReservationServiceServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterReservationServiceServer(s grpc.ServiceRegistrar, srv ReservationServiceServer) {
|
||||||
|
s.RegisterService(&ReservationService_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ReservationService_ReservationNotificationStream_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
m := new(ReservationNotificationRequest)
|
||||||
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return srv.(ReservationServiceServer).ReservationNotificationStream(m, &reservationServiceReservationNotificationStreamServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReservationService_ReservationNotificationStreamServer interface {
|
||||||
|
Send(*ServerReservationNotification) error
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type reservationServiceReservationNotificationStreamServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *reservationServiceReservationNotificationStreamServer) Send(m *ServerReservationNotification) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ReservationService_OpenReservation_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(ServerOpenReservationRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ReservationServiceServer).OpenReservation(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/looprpc.ReservationService/OpenReservation",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ReservationServiceServer).OpenReservation(ctx, req.(*ServerOpenReservationRequest))
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
var ReservationService_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "looprpc.ReservationService",
|
||||||
|
HandlerType: (*ReservationServiceServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "OpenReservation",
|
||||||
|
Handler: _ReservationService_OpenReservation_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "ReservationNotificationStream",
|
||||||
|
Handler: _ReservationService_ReservationNotificationStream_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: "reservation.proto",
|
||||||
|
}
|
Loading…
Reference in New Issue