Compare commits

..

No commits in common. 'master' and 'v0.26.4-beta' have entirely different histories.

@ -20,7 +20,7 @@ env:
# If you change this value, please change it in the following files as well:
# /Dockerfile
GO_VERSION: 1.21.10
GO_VERSION: 1.20.4
jobs:
########################

@ -53,7 +53,7 @@ docker exec -it loopd loop out --channel <channel-id-you-want-to-use> --amt <amo
Things to note about this docker command:
* `docker exec` runs a command on an already-running container. In this case `docker exec loopd` says effectively 'run the rest of this command-line as a command on the already-running container 'loopd'.
* The `-it` flags tell docker to run the command interactively and act like it's using a terminal. This helps with commands that do more than just write to stdout.
* The `-it` flags tell docker to run the command interatively and act like it's using a terminal. This helps with commands that do more than just write to stdout.
* The remainder `loop out --channel <channel-id-you-want-to-use> --amt <amount-you-want-to-loop-out>` is the actual loop command you want to run. All the regular `loop` documentation applies to this bit.

@ -1,4 +1,4 @@
FROM --platform=${BUILDPLATFORM} golang:1.22-alpine as builder
FROM --platform=${BUILDPLATFORM} golang:1.20.4-alpine as builder
# Copy in the local repository to build from.
COPY . /go/src/github.com/lightningnetwork/loop

@ -34,11 +34,7 @@ ifneq ($(workers),)
LINT_WORKERS = --concurrency=$(workers)
endif
DOCKER_TOOLS = docker run \
--rm \
-v $(shell bash -c "go env GOCACHE || (mkdir -p /tmp/go-cache; echo /tmp/go-cache)"):/tmp/build/.cache \
-v $(shell bash -c "go env GOMODCACHE || (mkdir -p /tmp/go-modcache; echo /tmp/go-modcache)"):/tmp/build/.modcache \
-v $$(pwd):/build loop-tools
DOCKER_TOOLS = docker run -v $$(pwd):/build loop-tools
GREEN := "\\033[0;32m"
NC := "\\033[0m"

@ -12,16 +12,12 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/aperture/l402"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
@ -46,23 +42,19 @@ var (
// is too soon for us.
ErrExpiryTooFar = errors.New("swap expiry too far")
// ErrInsufficientBalance indicates insufficient confirmed balance to
// publish a swap.
ErrInsufficientBalance = errors.New("insufficient confirmed balance")
// serverRPCTimeout is the maximum time a gRPC request to the server
// should be allowed to take.
serverRPCTimeout = 30 * time.Second
// globalCallTimeout is the maximum time any call of the client to the
// server is allowed to take, including the time it may take to get
// and pay for an L402 token.
globalCallTimeout = serverRPCTimeout + l402.PaymentTimeout
// and pay for an LSAT token.
globalCallTimeout = serverRPCTimeout + lsat.PaymentTimeout
// probeTimeout is the maximum time until a probe is allowed to take.
probeTimeout = 3 * time.Minute
repushDelay = 1 * time.Second
republishDelay = 10 * time.Second
// MinerFeeEstimationFailed is a magic number that is returned in a
// quote call as the miner fee if the fee estimation in lnd's wallet
@ -76,11 +68,6 @@ type Client struct {
started uint32 // To be used atomically.
errChan chan error
// abandonChans allows for accessing a swap's abandon channel by
// providing its swap hash. This map is used to look up the abandon
// channel of a swap if the client requests to abandon it.
abandonChans map[lntypes.Hash]chan struct{}
lndServices *lndclient.LndServices
sweeper *sweep.Sweeper
executor *executor
@ -111,13 +98,13 @@ type ClientConfig struct {
// Lnd is an instance of the lnd proxy.
Lnd *lndclient.LndServices
// MaxL402Cost is the maximum price we are willing to pay to the server
// MaxLsatCost is the maximum price we are willing to pay to the server
// for the token.
MaxL402Cost btcutil.Amount
MaxLsatCost btcutil.Amount
// MaxL402Fee is the maximum that we are willing to pay in routing fees
// MaxLsatFee is the maximum that we are willing to pay in routing fees
// to obtain the token.
MaxL402Fee btcutil.Amount
MaxLsatFee btcutil.Amount
// LoopOutMaxParts defines the maximum number of parts that may be used
// for a loop out swap. When greater than one, a multi-part payment may
@ -135,15 +122,14 @@ type ClientConfig struct {
// NewClient returns a new instance to initiate swaps with.
func NewClient(dbDir string, loopDB loopdb.SwapStore,
sweeperDb sweepbatcher.BatcherStore, cfg *ClientConfig) (
*Client, func(), error) {
cfg *ClientConfig) (*Client, func(), error) {
l402Store, err := l402.NewFileStore(dbDir)
lsatStore, err := lsat.NewFileStore(dbDir)
if err != nil {
return nil, nil, err
}
swapServerClient, err := newSwapServerClient(cfg, l402Store)
swapServerClient, err := newSwapServerClient(cfg, lsatStore)
if err != nil {
return nil, nil, err
}
@ -152,8 +138,7 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
LndServices: cfg.Lnd,
Server: swapServerClient,
Store: loopDB,
Conn: swapServerClient.conn,
L402Store: l402Store,
LsatStore: lsatStore,
CreateExpiryTimer: func(d time.Duration) <-chan time.Time {
return time.NewTimer(d).C
},
@ -164,44 +149,27 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
Lnd: cfg.Lnd,
}
verifySchnorrSig := func(pubKey *btcec.PublicKey, hash, sig []byte) error {
schnorrSig, err := schnorr.ParseSignature(sig)
if err != nil {
return err
}
if !schnorrSig.Verify(hash, pubKey) {
return fmt.Errorf("invalid signature")
}
return nil
}
sweepStore, err := sweepbatcher.NewSweepFetcherFromSwapStore(
loopDB, cfg.Lnd.ChainParams,
)
if err != nil {
return nil, nil, fmt.Errorf("sweepbatcher."+
"NewSweepFetcherFromSwapStore failed: %w", err)
}
batcher := sweepbatcher.NewBatcher(
cfg.Lnd.WalletKit, cfg.Lnd.ChainNotifier, cfg.Lnd.Signer,
swapServerClient.MultiMuSig2SignSweep, verifySchnorrSig,
cfg.Lnd.ChainParams, sweeperDb, sweepStore,
)
executor := newExecutor(&executorConfig{
lnd: cfg.Lnd,
store: loopDB,
sweeper: sweeper,
batcher: batcher,
createExpiryTimer: config.CreateExpiryTimer,
loopOutMaxParts: cfg.LoopOutMaxParts,
totalPaymentTimeout: cfg.TotalPaymentTimeout,
maxPaymentRetries: cfg.MaxPaymentRetries,
cancelSwap: swapServerClient.CancelLoopOutSwap,
verifySchnorrSig: verifySchnorrSig,
verifySchnorrSig: func(pubKey *btcec.PublicKey, hash, sig []byte) error {
schnorrSig, err := schnorr.ParseSignature(sig)
if err != nil {
return err
}
if !schnorrSig.Verify(hash, pubKey) {
return fmt.Errorf("invalid signature")
}
return nil
},
})
client := &Client{
@ -211,7 +179,6 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
sweeper: sweeper,
executor: executor,
resumeReady: make(chan struct{}),
abandonChans: make(map[lntypes.Hash]chan struct{}),
}
cleanup := func() {
@ -222,11 +189,6 @@ func NewClient(dbDir string, loopDB loopdb.SwapStore,
return client, cleanup, nil
}
// GetConn returns the gRPC connection to the server.
func (s *Client) GetConn() *grpc.ClientConn {
return s.clientConfig.Conn
}
// FetchSwaps returns all loop in and out swaps currently in the database.
func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
loopOutSwaps, err := s.Store.FetchLoopOutSwaps(ctx)
@ -252,7 +214,7 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
LastUpdate: swp.LastUpdateTime(),
}
htlc, err := utils.GetHtlc(
htlc, err := GetHtlc(
swp.Hash, &swp.Contract.SwapContract,
s.lndServices.ChainParams,
)
@ -285,7 +247,7 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
LastUpdate: swp.LastUpdateTime(),
}
htlc, err := utils.GetHtlc(
htlc, err := GetHtlc(
swp.Hash, &swp.Contract.SwapContract,
s.lndServices.ChainParams,
)
@ -355,10 +317,10 @@ func (s *Client) Run(ctx context.Context, statusChan chan<- SwapInfo) error {
}()
// Main event loop.
err = s.executor.run(mainCtx, statusChan, s.abandonChans)
err = s.executor.run(mainCtx, statusChan)
// Consider canceled as happy flow.
if errors.Is(err, context.Canceled) {
if err == context.Canceled {
err = nil
}
@ -412,12 +374,6 @@ func (s *Client) resumeSwaps(ctx context.Context,
continue
}
// Store the swap's abandon channel so that the client can
// abandon the swap by providing the swap hash.
s.executor.Lock()
s.abandonChans[swap.hash] = swap.abandonChan
s.executor.Unlock()
s.executor.initiateSwap(ctx, swap)
}
}
@ -560,7 +516,7 @@ func (s *Client) getLoopOutSweepFee(ctx context.Context, confTarget int32) (
return 0, err
}
scriptVersion := utils.GetHtlcScriptVersion(
scriptVersion := GetHtlcScriptVersion(
loopdb.CurrentProtocolVersion(),
)
@ -622,10 +578,6 @@ func (s *Client) LoopIn(globalCtx context.Context,
}
swap := initResult.swap
s.executor.Lock()
s.abandonChans[swap.hash] = swap.abandonChan
s.executor.Unlock()
// Post swap to the main loop.
s.executor.initiateSwap(globalCtx, swap)
@ -683,8 +635,7 @@ func (s *Client) LoopInQuote(ctx context.Context,
// Because the Private flag is set, we'll generate our own
// set of hop hints and use that
request.RouteHints, err = SelectHopHints(
ctx, s.lndServices.Client, request.Amount,
DefaultMaxHopHints, includeNodes,
ctx, s.lndServices, request.Amount, DefaultMaxHopHints, includeNodes,
)
if err != nil {
return nil, err
@ -752,7 +703,7 @@ func (s *Client) estimateFee(ctx context.Context, amt btcutil.Amount,
// Generate a dummy address for fee estimation.
witnessProg := [32]byte{}
scriptVersion := utils.GetHtlcScriptVersion(
scriptVersion := GetHtlcScriptVersion(
loopdb.CurrentProtocolVersion(),
)
@ -802,26 +753,3 @@ func (s *Client) Probe(ctx context.Context, req *ProbeRequest) error {
req.RouteHints,
)
}
// AbandonSwap sends a signal on the abandon channel of the swap identified by
// the passed swap hash. This will cause the swap to abandon itself.
func (s *Client) AbandonSwap(ctx context.Context,
req *AbandonSwapRequest) error {
if req == nil {
return errors.New("no request provided")
}
s.executor.Lock()
defer s.executor.Unlock()
select {
case s.abandonChans[req.SwapHash] <- struct{}{}:
case <-ctx.Done():
return ctx.Err()
default:
// This is to avoid writing to a full channel.
}
return nil
}

@ -13,7 +13,6 @@ import (
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/test"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
@ -106,7 +105,7 @@ func TestLoopOutFailOffchain(t *testing.T) {
ctx.finish()
}
// TestLoopOutFailWrongAmount asserts that the client checks the server invoice
// TestLoopOutWrongAmount asserts that the client checks the server invoice
// amounts.
func TestLoopOutFailWrongAmount(t *testing.T) {
defer test.Guard(t)()
@ -147,6 +146,8 @@ func TestLoopOutFailWrongAmount(t *testing.T) {
// TestLoopOutResume tests that swaps in various states are properly resumed
// after a restart.
func TestLoopOutResume(t *testing.T) {
defer test.Guard(t)()
defaultConfs := loopdb.DefaultLoopOutHtlcConfirmations
storedVersion := []loopdb.ProtocolVersion{
@ -278,7 +279,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed,
preimageRevealed, int32(confs),
)
htlc, err := utils.GetHtlc(
htlc, err := GetHtlc(
hash, &pendingSwap.Contract.SwapContract,
&chaincfg.TestNet3Params,
)
@ -303,7 +304,7 @@ func testLoopOutResume(t *testing.T, confs uint32, expired, preimageRevealed,
func(r error) {},
func(r error) {},
preimageRevealed,
confIntent, utils.GetHtlcScriptVersion(protocolVersion),
confIntent, GetHtlcScriptVersion(protocolVersion),
)
}
@ -316,28 +317,15 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
signalPrepaymentResult(nil)
ctx.AssertRegisterSpendNtfn(confIntent.PkScript)
// Assert that a call to track payment was sent, and respond with status
// in flight so that our swap will push its preimage to the server.
ctx.trackPayment(lnrpc.Payment_IN_FLIGHT)
// We need to notify the height, as the loopout is going to attempt a
// sweep when a new block is received.
err := ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1)
require.NoError(ctx.Context.T, err)
// Publish tick.
ctx.expiryChan <- testTime
// One spend notifier is registered by batch to watch primary sweep.
ctx.AssertRegisterSpendNtfn(confIntent.PkScript)
ctx.AssertEpochListeners(2)
// Mock the blockheight again as that's when the batch will broadcast
// the tx.
err = ctx.Lnd.NotifyHeight(ctx.Lnd.Height + 1)
require.NoError(ctx.Context.T, err)
// Expect a signing request in the non taproot case.
if scriptVersion != swap.HtlcV3 {
<-ctx.Context.Lnd.SignOutputRawChannel
@ -352,7 +340,14 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
// preimage before sweeping in order for the server to trust us with
// our MuSig2 signing attempts.
if scriptVersion == swap.HtlcV3 {
ctx.assertPreimagePush(ctx.store.LoopOutSwaps[hash].Preimage)
ctx.assertPreimagePush(ctx.store.loopOutSwaps[hash].Preimage)
// Try MuSig2 signing first and fail it so that we go for a
// normal sweep.
for i := 0; i < maxMusigSweepRetries; i++ {
ctx.expiryChan <- testTime
ctx.assertPreimagePush(ctx.store.loopOutSwaps[hash].Preimage)
}
<-ctx.Context.Lnd.SignOutputRawChannel
}
@ -393,8 +388,6 @@ func testLoopOutSuccess(ctx *testContext, amt btcutil.Amount, hash lntypes.Hash,
ctx.NotifySpend(sweepTx, 0)
ctx.AssertRegisterConf(true, 3)
ctx.assertStatus(loopdb.StateSuccess)
ctx.assertStoreFinished(loopdb.StateSuccess)

@ -1,232 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
)
var instantOutCommand = cli.Command{
Name: "instantout",
Usage: "perform an instant off-chain to on-chain swap (looping out)",
Description: `
Attempts to instantly loop out into the backing lnd's wallet. The amount
will be chosen via the cli.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "channel",
Usage: "the comma-separated list of short " +
"channel IDs of the channels to loop out",
},
cli.StringFlag{
Name: "addr",
Usage: "the optional address that the looped out funds " +
"should be sent to, if let blank the funds " +
"will go to lnd's wallet",
},
},
Action: instantOut,
}
func instantOut(ctx *cli.Context) error {
// Parse outgoing channel set. Don't string split if the flag is empty.
// Otherwise, strings.Split returns a slice of length one with an empty
// element.
var outgoingChanSet []uint64
if ctx.IsSet("channel") {
chanStrings := strings.Split(ctx.String("channel"), ",")
for _, chanString := range chanStrings {
chanID, err := strconv.ParseUint(chanString, 10, 64)
if err != nil {
return fmt.Errorf("error parsing channel id "+
"\"%v\"", chanString)
}
outgoingChanSet = append(outgoingChanSet, chanID)
}
}
// First set up the swap client itself.
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
// Now we fetch all the confirmed reservations.
reservations, err := client.ListReservations(
context.Background(), &looprpc.ListReservationsRequest{},
)
if err != nil {
return err
}
var (
confirmedReservations []*looprpc.ClientReservation
totalAmt int64
idx int
)
for _, res := range reservations.Reservations {
if res.State != string(reservation.Confirmed) {
continue
}
confirmedReservations = append(confirmedReservations, res)
}
if len(confirmedReservations) == 0 {
fmt.Printf("No confirmed reservations found \n")
return nil
}
fmt.Printf("Available reservations: \n\n")
for _, res := range confirmedReservations {
idx++
fmt.Printf("Reservation %v: shortid %x, amt %v, expiry "+
"height %v \n", idx, res.ReservationId[:3], res.Amount,
res.Expiry)
totalAmt += int64(res.Amount)
}
fmt.Println()
fmt.Printf("Max amount to instant out: %v\n", totalAmt)
fmt.Println()
fmt.Println("Select reservations for instantout (e.g. '1,2,3')")
fmt.Println("Type 'ALL' to use all available reservations.")
var answer string
fmt.Scanln(&answer)
// Parse
var (
selectedReservations [][]byte
selectedAmt uint64
)
switch answer {
case "ALL":
for _, res := range confirmedReservations {
selectedReservations = append(
selectedReservations,
res.ReservationId,
)
selectedAmt += res.Amount
}
case "":
return fmt.Errorf("no reservations selected")
default:
selectedIndexes := strings.Split(answer, ",")
selectedIndexMap := make(map[int]struct{})
for _, idxStr := range selectedIndexes {
idx, err := strconv.Atoi(idxStr)
if err != nil {
return err
}
if idx < 0 {
return fmt.Errorf("invalid index %v", idx)
}
if idx > len(confirmedReservations) {
return fmt.Errorf("invalid index %v", idx)
}
if _, ok := selectedIndexMap[idx]; ok {
return fmt.Errorf("duplicate index %v", idx)
}
selectedReservations = append(
selectedReservations,
confirmedReservations[idx-1].ReservationId,
)
selectedIndexMap[idx] = struct{}{}
selectedAmt += confirmedReservations[idx-1].Amount
}
}
// Now that we have the selected reservations we can estimate the
// fee-rates.
quote, err := client.InstantOutQuote(
context.Background(), &looprpc.InstantOutQuoteRequest{
Amt: selectedAmt,
NumReservations: int32(len(selectedReservations)),
},
)
if err != nil {
return err
}
fmt.Println()
fmt.Printf(satAmtFmt, "Estimated on-chain fee:", quote.SweepFeeSat)
fmt.Printf(satAmtFmt, "Service fee:", quote.ServiceFeeSat)
fmt.Println()
fmt.Printf("CONTINUE SWAP? (y/n): ")
fmt.Scanln(&answer)
if answer != "y" {
return errors.New("swap canceled")
}
fmt.Println("Starting instant swap out")
// Now we can request the instant out swap.
instantOutRes, err := client.InstantOut(
context.Background(),
&looprpc.InstantOutRequest{
ReservationIds: selectedReservations,
OutgoingChanSet: outgoingChanSet,
DestAddr: ctx.String("addr"),
},
)
if err != nil {
return err
}
fmt.Printf("Instant out swap initiated with ID: %x, State: %v \n",
instantOutRes.InstantOutHash, instantOutRes.State)
if instantOutRes.SweepTxId != "" {
fmt.Printf("Sweepless sweep tx id: %v \n",
instantOutRes.SweepTxId)
}
return nil
}
var listInstantOutsCommand = cli.Command{
Name: "listinstantouts",
Usage: "list all instant out swaps",
Description: `
List all instant out swaps.
`,
Action: listInstantOuts,
}
func listInstantOuts(ctx *cli.Context) error {
// First set up the swap client itself.
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.ListInstantOuts(
context.Background(), &looprpc.ListInstantOutsRequest{},
)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}

@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"math"
"strconv"
"strings"
"time"
@ -16,13 +15,6 @@ import (
"github.com/urfave/cli"
)
var (
channelFlag = cli.StringFlag{
Name: "channel",
Usage: "the comma-separated list of short " +
"channel IDs of the channels to loop out",
}
)
var loopOutCommand = cli.Command{
Name: "out",
Usage: "perform an off-chain to on-chain swap (looping out)",
@ -36,6 +28,11 @@ var loopOutCommand = cli.Command{
Optionally a BASE58/bech32 encoded bitcoin destination address may be
specified. If not specified, a new wallet address will be generated.`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "channel",
Usage: "the comma-separated list of short " +
"channel IDs of the channels to loop out",
},
cli.StringFlag{
Name: "addr",
Usage: "the optional address that the looped out funds " +
@ -93,18 +90,9 @@ var loopOutCommand = cli.Command{
"Not setting this flag therefore might " +
"result in a lower swap fee",
},
cli.DurationFlag{
Name: "payment_timeout",
Usage: "the timeout for each individual off-chain " +
"payment attempt. If not set, the default " +
"timeout of 1 hour will be used. As the " +
"payment might be retried, the actual total " +
"time may be longer",
},
forceFlag,
labelFlag,
verboseFlag,
channelFlag,
},
Action: loopOut,
}
@ -244,29 +232,9 @@ func loopOut(ctx *cli.Context) error {
}
}
var paymentTimeout int64
if ctx.IsSet("payment_timeout") {
parsedTimeout := ctx.Duration("payment_timeout")
if parsedTimeout.Truncate(time.Second) != parsedTimeout {
return fmt.Errorf("payment timeout must be a " +
"whole number of seconds")
}
paymentTimeout = int64(parsedTimeout.Seconds())
if paymentTimeout <= 0 {
return fmt.Errorf("payment timeout must be a " +
"positive value")
}
if paymentTimeout > math.MaxUint32 {
return fmt.Errorf("payment timeout is too large")
}
}
resp, err := client.LoopOut(context.Background(), &looprpc.LoopOutRequest{
Amt: int64(amt),
Dest: destAddr,
IsExternalAddr: destAddr != "",
Account: account,
AccountAddrType: accountAddrType,
MaxMinerFee: int64(limits.maxMinerFee),
@ -280,7 +248,6 @@ func loopOut(ctx *cli.Context) error {
SwapPublicationDeadline: uint64(swapDeadline.Unix()),
Label: label,
Initiator: defaultInitiator,
PaymentTimeout: uint32(paymentTimeout),
})
if err != nil {
return err

@ -26,8 +26,8 @@ type printableToken struct {
var listAuthCommand = cli.Command{
Name: "listauth",
Usage: "list all L402 tokens",
Description: "Shows a list of all L402 tokens that loopd has paid for",
Usage: "list all LSAT tokens",
Description: "Shows a list of all LSAT tokens that loopd has paid for",
Action: listAuth,
}
@ -38,7 +38,7 @@ func listAuth(ctx *cli.Context) error {
}
defer cleanup()
resp, err := client.GetL402Tokens(
resp, err := client.GetLsatTokens(
context.Background(), &looprpc.TokensRequest{},
)
if err != nil {

@ -147,8 +147,7 @@ func main() {
monitorCommand, quoteCommand, listAuthCommand,
listSwapsCommand, swapInfoCommand, getLiquidityParamsCommand,
setLiquidityRuleCommand, suggestSwapCommand, setParamsCommand,
getInfoCommand, abandonSwapCommand, reservationsCommands,
instantOutCommand, listInstantOutsCommand,
getInfoCommand,
}
err := app.Run(os.Args)

@ -1,55 +0,0 @@
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
}

@ -4,12 +4,9 @@ import (
"context"
"encoding/hex"
"fmt"
"strconv"
"strings"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli"
)
@ -19,23 +16,6 @@ var listSwapsCommand = cli.Command{
Description: "Allows the user to get a list of all swaps that are " +
"currently stored in the database",
Action: listSwaps,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "loop_out_only",
Usage: "only list swaps that are loop out swaps",
},
cli.BoolFlag{
Name: "loop_in_only",
Usage: "only list swaps that are loop in swaps",
},
cli.BoolFlag{
Name: "pending_only",
Usage: "only list pending swaps",
},
labelFlag,
channelFlag,
lastHopFlag,
},
}
func listSwaps(ctx *cli.Context) error {
@ -45,64 +25,8 @@ func listSwaps(ctx *cli.Context) error {
}
defer cleanup()
if ctx.Bool("loop_out_only") && ctx.Bool("loop_in_only") {
return fmt.Errorf("only one of loop_out_only and loop_in_only " +
"can be set")
}
filter := &looprpc.ListSwapsFilter{}
// Set the swap type filter.
switch {
case ctx.Bool("loop_out_only"):
filter.SwapType = looprpc.ListSwapsFilter_LOOP_OUT
case ctx.Bool("loop_in_only"):
filter.SwapType = looprpc.ListSwapsFilter_LOOP_IN
}
// Set the pending only filter.
filter.PendingOnly = ctx.Bool("pending_only")
// Parse outgoing channel set. Don't string split if the flag is empty.
// Otherwise, strings.Split returns a slice of length one with an empty
// element.
var outgoingChanSet []uint64
if ctx.IsSet(channelFlag.Name) {
chanStrings := strings.Split(ctx.String(channelFlag.Name), ",")
for _, chanString := range chanStrings {
chanID, err := strconv.ParseUint(chanString, 10, 64)
if err != nil {
return fmt.Errorf("error parsing channel id "+
"\"%v\"", chanString)
}
outgoingChanSet = append(outgoingChanSet, chanID)
}
filter.OutgoingChanSet = outgoingChanSet
}
// Parse last hop.
var lastHop []byte
if ctx.IsSet(lastHopFlag.Name) {
lastHopVertex, err := route.NewVertexFromStr(
ctx.String(lastHopFlag.Name),
)
if err != nil {
return err
}
lastHop = lastHopVertex[:]
filter.LoopInLastHop = lastHop
}
// Parse label.
if ctx.IsSet(labelFlag.Name) {
filter.Label = ctx.String(labelFlag.Name)
}
resp, err := client.ListSwaps(
context.Background(), &looprpc.ListSwapsRequest{
ListSwapFilter: filter,
},
context.Background(), &looprpc.ListSwapsRequest{},
)
if err != nil {
return err
@ -166,73 +90,3 @@ func swapInfo(ctx *cli.Context) error {
printRespJSON(resp)
return nil
}
var abandonSwapCommand = cli.Command{
Name: "abandonswap",
Usage: "abandon a swap with a given swap hash",
Description: "This command overrides the database and abandons a " +
"swap with a given swap hash.\n\n" +
"!!! This command might potentially lead to loss of funds if " +
"it is applied to swaps that are still waiting for pending " +
"user funds. Before executing this command make sure that " +
"no funds are locked by the swap.",
ArgsUsage: "ID",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "i_know_what_i_am_doing",
Usage: "Specify this flag if you made sure that you " +
"read and understood the following " +
"consequence of applying this command.",
},
},
Action: abandonSwap,
}
func abandonSwap(ctx *cli.Context) error {
args := ctx.Args()
var id string
switch {
case ctx.IsSet("id"):
id = ctx.String("id")
case ctx.NArg() > 0:
id = args[0]
args = args.Tail() // nolint:wastedassign
default:
// Show command help if no arguments and flags were provided.
return cli.ShowCommandHelp(ctx, "abandonswap")
}
if len(id) != hex.EncodedLen(lntypes.HashSize) {
return fmt.Errorf("invalid swap ID")
}
idBytes, err := hex.DecodeString(id)
if err != nil {
return fmt.Errorf("cannot hex decode id: %v", err)
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
if !ctx.Bool("i_know_what_i_am_doing") {
return cli.ShowCommandHelp(ctx, "abandonswap")
}
resp, err := client.AbandonSwap(
context.Background(), &looprpc.AbandonSwapRequest{
Id: idBytes,
IKnowWhatIAmDoing: ctx.Bool("i_know_what_i_am_doing"),
},
)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}

@ -3,19 +3,17 @@ package loop
import (
"time"
"github.com/lightninglabs/aperture/l402"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"google.golang.org/grpc"
)
// clientConfig contains config items for the swap client.
type clientConfig struct {
LndServices *lndclient.LndServices
Server swapServerClient
Conn *grpc.ClientConn
Store loopdb.SwapStore
L402Store l402.Store
LsatStore lsat.Store
CreateExpiryTimer func(expiry time.Duration) <-chan time.Time
LoopOutMaxParts uint32
}

@ -1,204 +0,0 @@
package loop
import (
"context"
"fmt"
"time"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
)
const (
// costMigrationID is the identifier for the cost migration.
costMigrationID = "cost_migration"
// paymentBatchSize is the maximum number of payments we'll fetch in
// one go.
paymentBatchSize = 1000
)
// CalculateLoopOutCost calculates the total cost of a loop out swap. It will
// correctly account for the on-chain and off-chain fees that were paid and
// make sure that all costs are positive.
func CalculateLoopOutCost(params *chaincfg.Params, loopOutSwap *loopdb.LoopOut,
paymentFees map[lntypes.Hash]lnwire.MilliSatoshi) (loopdb.SwapCost,
error) {
// First make sure that this swap is actually finished.
if loopOutSwap.State().State.IsPending() {
return loopdb.SwapCost{}, fmt.Errorf("swap is not yet finished")
}
// We first need to decode the prepay invoice to get the prepay hash and
// the prepay amount.
_, _, hash, prepayAmount, err := swap.DecodeInvoice(
params, loopOutSwap.Contract.PrepayInvoice,
)
if err != nil {
return loopdb.SwapCost{}, fmt.Errorf("unable to decode the "+
"prepay invoice: %v", err)
}
// The swap hash is given and we don't need to get it from the
// swap invoice, however we'll decode it anyway to get the invoice amount
// that was paid in case we don't have the payment anymore.
_, _, swapHash, swapPaymentAmount, err := swap.DecodeInvoice(
params, loopOutSwap.Contract.SwapInvoice,
)
if err != nil {
return loopdb.SwapCost{}, fmt.Errorf("unable to decode the "+
"swap invoice: %v", err)
}
var (
cost loopdb.SwapCost
swapPaid, prepayPaid bool
)
// Now that we have the prepay and swap amount, we can calculate the
// total cost of the swap. Note that we only need to account for the
// server cost in case the swap was successful or if the sweep timed
// out. Otherwise the server didn't pull the off-chain htlc nor the
// prepay.
switch loopOutSwap.State().State {
case loopdb.StateSuccess:
cost.Server = swapPaymentAmount + prepayAmount -
loopOutSwap.Contract.AmountRequested
swapPaid = true
prepayPaid = true
case loopdb.StateFailSweepTimeout:
cost.Server = prepayAmount
prepayPaid = true
default:
cost.Server = 0
}
// Now attempt to look up the actual payments so we can calculate the
// total routing costs.
prepayPaymentFee, ok := paymentFees[hash]
if prepayPaid && ok {
cost.Offchain += prepayPaymentFee.ToSatoshis()
} else {
log.Debugf("Prepay payment %s is missing, won't account for "+
"routing fees", hash)
}
swapPaymentFee, ok := paymentFees[swapHash]
if swapPaid && ok {
cost.Offchain += swapPaymentFee.ToSatoshis()
} else {
log.Debugf("Swap payment %s is missing, won't account for "+
"routing fees", swapHash)
}
// For the on-chain cost, just make sure that the cost is positive.
cost.Onchain = loopOutSwap.State().Cost.Onchain
if cost.Onchain < 0 {
cost.Onchain *= -1
}
return cost, nil
}
// MigrateLoopOutCosts will calculate the correct cost for all loop out swaps
// and override the cost values of the last update in the database.
func MigrateLoopOutCosts(ctx context.Context, lnd lndclient.LndServices,
db loopdb.SwapStore) error {
migrationDone, err := db.HasMigration(ctx, costMigrationID)
if err != nil {
return err
}
if migrationDone {
log.Infof("Cost cleanup migration already done, skipping")
return nil
}
log.Infof("Starting cost cleanup migration")
startTs := time.Now()
defer func() {
log.Infof("Finished cost cleanup migration in %v",
time.Since(startTs))
}()
// First we'll fetch all loop out swaps from the database.
loopOutSwaps, err := db.FetchLoopOutSwaps(ctx)
if err != nil {
return err
}
// Gather payment fees to a map for easier lookup.
paymentFees := make(map[lntypes.Hash]lnwire.MilliSatoshi)
offset := uint64(0)
for {
payments, err := lnd.Client.ListPayments(
ctx, lndclient.ListPaymentsRequest{
Offset: offset,
MaxPayments: paymentBatchSize,
},
)
if err != nil {
return err
}
if len(payments.Payments) == 0 {
break
}
for _, payment := range payments.Payments {
paymentFees[payment.Hash] = payment.Fee
}
offset = payments.LastIndexOffset + 1
}
// Now we'll calculate the cost for each swap and finally update the
// costs in the database.
updatedCosts := make(map[lntypes.Hash]loopdb.SwapCost)
for _, loopOutSwap := range loopOutSwaps {
if loopOutSwap.State().State.IsPending() {
continue
}
cost, err := CalculateLoopOutCost(
lnd.ChainParams, loopOutSwap, paymentFees,
)
if err != nil {
// We don't want to fail loopd because of any old swap
// that we're unable to calculate the cost for. We'll
// warn though so that we can investigate further.
log.Warnf("Unable to calculate cost for swap %v: %v",
loopOutSwap.Hash, err)
continue
}
_, ok := updatedCosts[loopOutSwap.Hash]
if ok {
return fmt.Errorf("found a duplicate swap %v while "+
"updating costs", loopOutSwap.Hash)
}
updatedCosts[loopOutSwap.Hash] = cost
}
log.Infof("Updating costs for %d loop out swaps", len(updatedCosts))
err = db.BatchUpdateLoopOutSwapCosts(ctx, updatedCosts)
if err != nil {
return err
}
// Finally mark the migration as done.
return db.SetMigration(ctx, costMigrationID)
}

@ -1,184 +0,0 @@
package loop
import (
"context"
"testing"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
// TestCalculateLoopOutCost tests the CalculateLoopOutCost function.
func TestCalculateLoopOutCost(t *testing.T) {
// Set up test context objects.
lnd := test.NewMockLnd()
server := newServerMock(lnd)
store := loopdb.NewStoreMock(t)
cfg := &swapConfig{
lnd: &lnd.LndServices,
store: store,
server: server,
}
height := int32(600)
req := *testRequest
initResult, err := newLoopOutSwap(
context.Background(), cfg, height, &req,
)
require.NoError(t, err)
swap, err := store.FetchLoopOutSwap(
context.Background(), initResult.swap.hash,
)
require.NoError(t, err)
// Override the chain cost so it's negative.
const expectedChainCost = btcutil.Amount(1000)
// Now we have the swap and prepay invoices so let's calculate the
// costs without providing the payments first, so we don't account for
// any routing fees.
paymentFees := make(map[lntypes.Hash]lnwire.MilliSatoshi)
_, err = CalculateLoopOutCost(lnd.ChainParams, swap, paymentFees)
// We expect that the call fails as the swap isn't finished yet.
require.Error(t, err)
// Override the swap state to make it look like the swap is finished
// and make the chain cost negative too, so we can test that it'll be
// corrected to be positive in the cost calculation.
swap.Events = append(
swap.Events, &loopdb.LoopEvent{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateSuccess,
Cost: loopdb.SwapCost{
Onchain: -expectedChainCost,
},
},
},
)
costs, err := CalculateLoopOutCost(lnd.ChainParams, swap, paymentFees)
require.NoError(t, err)
expectedServerCost := server.swapInvoiceAmt + server.prepayInvoiceAmt -
swap.Contract.AmountRequested
require.Equal(t, expectedServerCost, costs.Server)
require.Equal(t, btcutil.Amount(0), costs.Offchain)
require.Equal(t, expectedChainCost, costs.Onchain)
// Now add the two payments to the payments map and calculate the costs
// again. We expect that the routng fees are now accounted for.
paymentFees[server.swapHash] = lnwire.NewMSatFromSatoshis(44)
paymentFees[server.prepayHash] = lnwire.NewMSatFromSatoshis(11)
costs, err = CalculateLoopOutCost(lnd.ChainParams, swap, paymentFees)
require.NoError(t, err)
expectedOffchainCost := btcutil.Amount(44 + 11)
require.Equal(t, expectedServerCost, costs.Server)
require.Equal(t, expectedOffchainCost, costs.Offchain)
require.Equal(t, expectedChainCost, costs.Onchain)
// Now override the last update to make the swap timed out at the HTLC
// sweep. We expect that the chain cost won't change, and only the
// prepay will be accounted for.
swap.Events[0] = &loopdb.LoopEvent{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateFailSweepTimeout,
Cost: loopdb.SwapCost{
Onchain: 0,
},
},
}
costs, err = CalculateLoopOutCost(lnd.ChainParams, swap, paymentFees)
require.NoError(t, err)
expectedServerCost = server.prepayInvoiceAmt
expectedOffchainCost = btcutil.Amount(11)
require.Equal(t, expectedServerCost, costs.Server)
require.Equal(t, expectedOffchainCost, costs.Offchain)
require.Equal(t, btcutil.Amount(0), costs.Onchain)
}
// TestCostMigration tests the cost migration for loop out swaps.
func TestCostMigration(t *testing.T) {
// Set up test context objects.
lnd := test.NewMockLnd()
server := newServerMock(lnd)
store := loopdb.NewStoreMock(t)
cfg := &swapConfig{
lnd: &lnd.LndServices,
store: store,
server: server,
}
height := int32(600)
req := *testRequest
initResult, err := newLoopOutSwap(
context.Background(), cfg, height, &req,
)
require.NoError(t, err)
// Override the chain cost so it's negative.
const expectedChainCost = btcutil.Amount(1000)
// Override the swap state to make it look like the swap is finished
// and make the chain cost negative too, so we can test that it'll be
// corrected to be positive in the cost calculation.
err = store.UpdateLoopOut(
context.Background(), initResult.swap.hash, time.Now(),
loopdb.SwapStateData{
State: loopdb.StateSuccess,
Cost: loopdb.SwapCost{
Onchain: -expectedChainCost,
},
},
)
require.NoError(t, err)
// Add the two mocked payment to LND. Note that we only care about the
// fees here, so we don't need to provide the full payment details.
lnd.Payments = []lndclient.Payment{
{
Hash: server.swapHash,
Fee: lnwire.NewMSatFromSatoshis(44),
},
{
Hash: server.prepayHash,
Fee: lnwire.NewMSatFromSatoshis(11),
},
}
// Now we can run the migration.
err = MigrateLoopOutCosts(context.Background(), lnd.LndServices, store)
require.NoError(t, err)
// Finally check that the swap cost has been updated correctly.
swap, err := store.FetchLoopOutSwap(
context.Background(), initResult.swap.hash,
)
require.NoError(t, err)
expectedServerCost := server.swapInvoiceAmt + server.prepayInvoiceAmt -
swap.Contract.AmountRequested
costs := swap.Events[0].Cost
expectedOffchainCost := btcutil.Amount(44 + 11)
require.Equal(t, expectedServerCost, costs.Server)
require.Equal(t, expectedOffchainCost, costs.Offchain)
require.Equal(t, expectedChainCost, costs.Onchain)
// Now run the migration again to make sure it doesn't fail. This also
// indicates that the migration did not run the second time as
// otherwise the store mocks SetMigration function would fail.
err = MigrateLoopOutCosts(context.Background(), lnd.LndServices, store)
require.NoError(t, err)
}

@ -2,7 +2,6 @@ package loop
import (
"context"
"errors"
"fmt"
"strings"
"sync"
@ -13,8 +12,6 @@ import (
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/queue"
)
@ -24,8 +21,6 @@ type executorConfig struct {
sweeper *sweep.Sweeper
batcher *sweepbatcher.Batcher
store loopdb.SwapStore
createExpiryTimer func(expiry time.Duration) <-chan time.Time
@ -50,8 +45,6 @@ type executor struct {
currentHeight uint32
ready chan struct{}
sync.Mutex
executorConfig
}
@ -67,14 +60,12 @@ func newExecutor(cfg *executorConfig) *executor {
// run starts the executor event loop. It accepts and executes new swaps,
// providing them with required config data.
func (s *executor) run(mainCtx context.Context,
statusChan chan<- SwapInfo,
abandonChans map[lntypes.Hash]chan struct{}) error {
statusChan chan<- SwapInfo) error {
var (
err error
blockEpochChan <-chan int32
blockErrorChan <-chan error
batcherErrChan chan error
)
for {
@ -125,21 +116,6 @@ func (s *executor) run(mainCtx context.Context,
return mainCtx.Err()
}
batcherErrChan = make(chan error, 1)
s.wg.Add(1)
go func() {
defer s.wg.Done()
err := s.batcher.Run(mainCtx)
if err != nil {
select {
case batcherErrChan <- err:
case <-mainCtx.Done():
}
}
}()
// Start main event loop.
log.Infof("Starting event loop at height %v", height)
@ -173,33 +149,20 @@ func (s *executor) run(mainCtx context.Context,
defer s.wg.Done()
err := newSwap.execute(mainCtx, &executeConfig{
statusChan: statusChan,
sweeper: s.sweeper,
batcher: s.batcher,
blockEpochChan: queue.ChanOut(),
timerFactory: s.executorConfig.createExpiryTimer,
loopOutMaxParts: s.executorConfig.loopOutMaxParts,
totalPaymentTimeout: s.executorConfig.totalPaymentTimeout,
maxPaymentRetries: s.executorConfig.maxPaymentRetries,
cancelSwap: s.executorConfig.cancelSwap,
verifySchnorrSig: s.executorConfig.verifySchnorrSig,
statusChan: statusChan,
sweeper: s.sweeper,
blockEpochChan: queue.ChanOut(),
timerFactory: s.executorConfig.createExpiryTimer,
loopOutMaxParts: s.executorConfig.loopOutMaxParts,
totalPaymentTimout: s.executorConfig.totalPaymentTimeout,
maxPaymentRetries: s.executorConfig.maxPaymentRetries,
cancelSwap: s.executorConfig.cancelSwap,
verifySchnorrSig: s.executorConfig.verifySchnorrSig,
}, height)
if err != nil && !errors.Is(
err, context.Canceled,
) {
if err != nil && err != context.Canceled {
log.Errorf("Execute error: %v", err)
}
// If a loop-in ended we have to remove its
// abandon channel from our abandonChans map
// since the swap finalized.
if swap, ok := newSwap.(*loopInSwap); ok {
s.Lock()
delete(abandonChans, swap.hash)
s.Unlock()
}
select {
case swapDoneChan <- swapID:
case <-mainCtx.Done():
@ -231,9 +194,6 @@ func (s *executor) run(mainCtx context.Context,
case err := <-blockErrorChan:
return fmt.Errorf("block error: %v", err)
case err := <-batcherErrChan:
return fmt.Errorf("batcher error: %v", err)
case <-mainCtx.Done():
return mainCtx.Err()
}

@ -31,7 +31,7 @@ func NewExampleFSMContext(service ExampleService,
service: service,
store: store,
}
exampleFSM.StateMachine = NewStateMachine(exampleFSM.GetStates(), 10)
exampleFSM.StateMachine = NewStateMachine(exampleFSM.GetStates())
return exampleFSM
}
@ -55,7 +55,7 @@ var (
// GetStates returns the states for the example FSM.
func (e *ExampleFSM) GetStates() States {
return States{
EmptyState: State{
Default: State{
Transitions: Transitions{
OnRequestStuff: InitFSM,
},

@ -2,11 +2,11 @@
stateDiagram-v2
[*] --> InitFSM: OnRequestStuff
InitFSM
InitFSM --> StuffSentOut: OnStuffSentOut
InitFSM --> StuffFailed: OnError
InitFSM --> StuffSentOut: OnStuffSentOut
StuffFailed
StuffSentOut
StuffSentOut --> StuffSuccess: OnStuffSuccess
StuffSentOut --> StuffFailed: OnError
StuffSentOut --> StuffSuccess: OnStuffSuccess
StuffSuccess
```

@ -243,82 +243,3 @@ func TestExampleFSMFlow(t *testing.T) {
})
}
}
// TestObserverAsyncWait tests the observer's WaitForStateAsync function.
func TestObserverAsyncWait(t *testing.T) {
testCases := []struct {
name string
waitTime time.Duration
blockTime time.Duration
expectTimeout bool
}{
{
name: "success",
waitTime: time.Second,
blockTime: time.Millisecond,
expectTimeout: false,
},
{
name: "timeout",
waitTime: time.Millisecond,
blockTime: time.Second,
expectTimeout: true,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
service := &mockService{
respondChan: make(chan bool),
}
store := &mockStore{}
exampleContext := NewExampleFSMContext(service, store)
cachedObserver := NewCachedObserver(100)
exampleContext.RegisterObserver(cachedObserver)
t0 := time.Now()
timeoutCtx, cancel := context.WithTimeout(
context.Background(), tc.waitTime,
)
defer cancel()
// Wait for the final state.
errChan := cachedObserver.WaitForStateAsync(
timeoutCtx, StuffSuccess, true,
)
go func() {
err := exampleContext.SendEvent(
OnRequestStuff,
newInitStuffRequest(),
)
require.NoError(t, err)
time.Sleep(tc.blockTime)
service.respondChan <- true
}()
timeout := false
select {
case <-timeoutCtx.Done():
timeout = true
case <-errChan:
}
require.Equal(t, tc.expectTimeout, timeout)
t1 := time.Now()
diff := t1.Sub(t0)
if tc.expectTimeout {
require.Less(t, diff, tc.blockTime)
} else {
require.Less(t, diff, tc.waitTime)
}
})
}
}

@ -13,15 +13,12 @@ var (
ErrWaitForStateTimedOut = errors.New(
"timed out while waiting for event",
)
ErrInvalidContextType = errors.New("invalid context")
ErrWaitingForStateEarlyAbortError = errors.New(
"waiting for state early abort",
)
ErrInvalidContextType = errors.New("invalid context")
)
const (
// EmptyState represents the default state of the system.
EmptyState StateType = ""
// Default represents the default state of the system.
Default StateType = ""
// NoOp represents a no-op event.
NoOp EventType = "NoOp"
@ -76,8 +73,6 @@ type Notification struct {
NextState StateType
// Event is the event that was processed.
Event EventType
// LastActionError is the error returned by the last action executed.
LastActionError error
}
// Observer is an interface that can be implemented by types that want to
@ -93,19 +88,19 @@ type StateMachine struct {
// ActionEntryFunc is a function that is called before an action is
// executed.
ActionEntryFunc func(Notification)
ActionEntryFunc func()
// ActionExitFunc is a function that is called after an action is
// executed, it is called with the EventType returned by the action.
ActionExitFunc func(NextEvent EventType)
// executed.
ActionExitFunc func()
// mutex ensures that only 1 event is processed by the state machine at
// any given time.
mutex sync.Mutex
// LastActionError is an error set by the last action executed.
LastActionError error
// DefaultObserver is the default observer that is notified when the
// state machine transitions between states.
DefaultObserver *CachedObserver
// previous represents the previous state.
previous StateType
@ -119,35 +114,13 @@ type StateMachine struct {
// observerMutex ensures that observers are only added or removed
// safely.
observerMutex sync.Mutex
// mutex ensures that only 1 event is processed by the state machine at
// any given time.
mutex sync.Mutex
}
// NewStateMachine creates a new state machine.
func NewStateMachine(states States, observerSize int) *StateMachine {
return NewStateMachineWithState(states, EmptyState, observerSize)
}
// NewStateMachineWithState creates a new state machine and sets the initial
// state.
func NewStateMachineWithState(states States, current StateType,
observerSize int) *StateMachine {
observers := []Observer{}
var defaultObserver *CachedObserver
if observerSize > 0 {
defaultObserver = NewCachedObserver(observerSize)
observers = append(observers, defaultObserver)
}
func NewStateMachine(states States) *StateMachine {
return &StateMachine{
States: states,
current: current,
DefaultObserver: defaultObserver,
observers: observers,
States: states,
observers: make([]Observer, 0),
}
}
@ -211,28 +184,23 @@ func (s *StateMachine) SendEvent(event EventType, eventCtx EventContext) error {
// current state.
state, err := s.getNextState(event)
if err != nil {
log.Errorf("unable to get next state: %v from event: "+
"%v, current state: %v", err, event, s.current)
return ErrEventRejected
}
// Notify the state machine's observers.
s.observerMutex.Lock()
notification := Notification{
PreviousState: s.previous,
NextState: s.current,
Event: event,
LastActionError: s.LastActionError,
}
for _, observer := range s.observers {
observer.Notify(notification)
observer.Notify(Notification{
PreviousState: s.previous,
NextState: s.current,
Event: event,
})
}
s.observerMutex.Unlock()
// Execute the state machines ActionEntryFunc.
if s.ActionEntryFunc != nil {
s.ActionEntryFunc(notification)
s.ActionEntryFunc()
}
// Execute the current state's entry function
@ -251,7 +219,7 @@ func (s *StateMachine) SendEvent(event EventType, eventCtx EventContext) error {
// Execute the state machines ActionExitFunc.
if s.ActionExitFunc != nil {
s.ActionExitFunc(nextEvent)
s.ActionExitFunc()
}
// If the next event is a no-op, we're done.
@ -306,36 +274,23 @@ func NoOpAction(_ EventContext) EventType {
}
// ErrConfigError is an error returned when the state machine is misconfigured.
type ErrConfigError struct {
msg string
}
// Error returns the error message.
func (e ErrConfigError) Error() string {
return fmt.Sprintf("config error: %s", e.msg)
}
type ErrConfigError error
// NewErrConfigError creates a new ErrConfigError.
func NewErrConfigError(msg string) ErrConfigError {
return ErrConfigError{
msg: msg,
}
return (ErrConfigError)(fmt.Errorf("config error: %s", msg))
}
// ErrWaitingForStateTimeout is an error returned when the state machine times
// out while waiting for a state.
type ErrWaitingForStateTimeout struct {
expected StateType
}
// Error returns the error message.
func (e ErrWaitingForStateTimeout) Error() string {
return fmt.Sprintf("waiting for state timed out: %s", e.expected)
}
type ErrWaitingForStateTimeout error
// NewErrWaitingForStateTimeout creates a new ErrWaitingForStateTimeout.
func NewErrWaitingForStateTimeout(expected StateType) ErrWaitingForStateTimeout {
return ErrWaitingForStateTimeout{
expected: expected,
}
func NewErrWaitingForStateTimeout(expected,
actual StateType) ErrWaitingForStateTimeout {
return (ErrWaitingForStateTimeout)(fmt.Errorf(
"waiting for state timeout: expected %s, actual: %s",
expected, actual,
))
}

@ -46,150 +46,42 @@ func (c *CachedObserver) GetCachedNotifications() []Notification {
return c.cachedNotifications.Get()
}
// WaitForStateOption is an option that can be passed to the WaitForState
// function.
type WaitForStateOption interface {
apply(*fsmOptions)
}
// fsmOptions is a struct that holds all options that can be passed to the
// WaitForState function.
type fsmOptions struct {
initialWait time.Duration
abortEarlyOnError bool
}
// InitialWaitOption is an option that can be passed to the WaitForState
// function to wait for a given duration before checking the state.
type InitialWaitOption struct {
initialWait time.Duration
}
// WithWaitForStateOption creates a new InitialWaitOption.
func WithWaitForStateOption(initialWait time.Duration) WaitForStateOption {
return &InitialWaitOption{
initialWait,
}
}
// apply implements the WaitForStateOption interface.
func (w *InitialWaitOption) apply(o *fsmOptions) {
o.initialWait = w.initialWait
}
// AbortEarlyOnErrorOption is an option that can be passed to the WaitForState
// function to abort early if an error occurs.
type AbortEarlyOnErrorOption struct {
abortEarlyOnError bool
}
// apply implements the WaitForStateOption interface.
func (a *AbortEarlyOnErrorOption) apply(o *fsmOptions) {
o.abortEarlyOnError = a.abortEarlyOnError
}
// WithAbortEarlyOnErrorOption creates a new AbortEarlyOnErrorOption.
func WithAbortEarlyOnErrorOption() WaitForStateOption {
return &AbortEarlyOnErrorOption{
abortEarlyOnError: true,
}
}
// WaitForState waits for the state machine to reach the given state.
// If the optional initialWait parameter is set, the function will wait for
// the given duration before checking the state. This is useful if the
// function is called immediately after sending an event to the state machine
// and the state machine needs some time to process the event.
func (c *CachedObserver) WaitForState(ctx context.Context,
timeout time.Duration, state StateType,
opts ...WaitForStateOption) error {
var options fsmOptions
for _, opt := range opts {
opt.apply(&options)
}
func (s *CachedObserver) WaitForState(ctx context.Context,
timeout time.Duration, state StateType) error {
// Wait for the initial wait duration if set.
if options.initialWait > 0 {
select {
case <-time.After(options.initialWait):
case <-ctx.Done():
return ctx.Err()
}
}
// Create a new context with a timeout.
timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
ch := c.WaitForStateAsync(timeoutCtx, state, options.abortEarlyOnError)
// Wait for either the condition to be met or for a timeout.
select {
case <-timeoutCtx.Done():
return NewErrWaitingForStateTimeout(state)
case err := <-ch:
return err
}
}
// Channel to notify when the desired state is reached
ch := make(chan struct{})
// WaitForStateAsync waits asynchronously until the passed context is canceled
// or the expected state is reached. The function returns a channel that will
// receive an error if the expected state is reached or an error occurred. If
// the context is canceled before the expected state is reached, the channel
// will receive an ErrWaitingForStateTimeout error.
func (c *CachedObserver) WaitForStateAsync(ctx context.Context, state StateType,
abortOnEarlyError bool) chan error {
// Channel to notify when the desired state is reached or an error
// occurred.
ch := make(chan error, 1)
// Wait on the notification condition variable asynchronously to avoid
// blocking the caller.
// Goroutine to wait on condition variable
go func() {
c.notificationMx.Lock()
defer c.notificationMx.Unlock()
// writeResult writes the result to the channel. If the context
// is canceled, an ErrWaitingForStateTimeout error is written
// to the channel.
writeResult := func(err error) {
select {
case <-ctx.Done():
ch <- NewErrWaitingForStateTimeout(
state,
)
case ch <- err:
}
}
s.notificationMx.Lock()
defer s.notificationMx.Unlock()
for {
// Check if the last state is the desired state.
if c.lastNotification.NextState == state {
writeResult(nil)
// Check if the last state is the desired state
if s.lastNotification.NextState == state {
ch <- struct{}{}
return
}
// Check if an error has occurred.
if c.lastNotification.Event == OnError {
lastErr := c.lastNotification.LastActionError
if abortOnEarlyError {
writeResult(lastErr)
return
}
}
// Otherwise use the conditional variable to wait for
// the next notification.
c.notificationCond.Wait()
// Otherwise, wait for the next notification
s.notificationCond.Wait()
}
}()
return ch
// Wait for either the condition to be met or for a timeout
select {
case <-timeoutCtx.Done():
return NewErrWaitingForStateTimeout(
state, s.lastNotification.NextState,
)
case <-ch:
return nil
}
}
// FixedSizeSlice is a slice with a fixed size.

@ -10,8 +10,6 @@ import (
"sort"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout"
"github.com/lightninglabs/loop/instantout/reservation"
)
func main() {
@ -43,20 +41,6 @@ func run() error {
return err
}
case "reservation":
reservationFSM := &reservation.FSM{}
err = writeMermaidFile(fp, reservationFSM.GetReservationStates())
if err != nil {
return err
}
case "instantout":
instantout := &instantout.FSM{}
err = writeMermaidFile(fp, instantout.GetV1ReservationStates())
if err != nil {
return err
}
default:
fmt.Println("Missing or wrong argument: fsm must be one of:")
fmt.Println("\treservations")

153
go.mod

@ -1,42 +1,42 @@
module github.com/lightninglabs/loop
require (
github.com/btcsuite/btcd v0.24.2-beta.rc1.0.20240403021926-ae5533602c46
github.com/btcsuite/btcd/btcec/v2 v2.3.3
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd v0.23.5-0.20230905170901-80f5a0ffdf36
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05
github.com/btcsuite/btcd/btcutil/psbt v1.1.8
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
github.com/btcsuite/btcwallet v0.16.10-0.20240404104514-b2f31f9045fb
github.com/btcsuite/btcwallet/wtxmgr v1.5.3
github.com/btcsuite/btcwallet v0.16.10-0.20230804184612-07be54bc22cf
github.com/btcsuite/btcwallet/wtxmgr v1.5.0
github.com/coreos/bbolt v1.3.3
github.com/davecgh/go-spew v1.1.1
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
github.com/fortytw2/leaktest v1.3.0
github.com/golang-migrate/migrate/v4 v4.17.0
github.com/golang-migrate/migrate/v4 v4.16.1
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3
github.com/jackc/pgconn v1.14.3
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438
github.com/jackc/pgconn v1.14.0
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
github.com/jessevdk/go-flags v1.4.0
github.com/lib/pq v1.10.9
github.com/lightninglabs/aperture v0.3.2-beta
github.com/lightninglabs/lndclient v0.18.0-1
github.com/lib/pq v1.10.7
github.com/lightninglabs/aperture v0.1.21-beta.0.20230705004936-87bb996a4030
github.com/lightninglabs/lndclient v0.17.0-1
github.com/lightninglabs/loop/swapserverrpc v1.0.5
github.com/lightningnetwork/lnd v0.18.0-beta.1
github.com/lightningnetwork/lnd v0.17.0-beta
github.com/lightningnetwork/lnd/cert v1.2.2
github.com/lightningnetwork/lnd/clock v1.1.1
github.com/lightningnetwork/lnd/queue v1.1.1
github.com/lightningnetwork/lnd/ticker v1.1.1
github.com/lightningnetwork/lnd/tor v1.1.2
github.com/ory/dockertest/v3 v3.10.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.8.2
github.com/urfave/cli v1.22.9
golang.org/x/net v0.23.0
google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.33.0
golang.org/x/net v0.10.0
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.30.0
gopkg.in/macaroon-bakery.v2 v2.1.0
gopkg.in/macaroon.v2 v2.1.0
modernc.org/sqlite v1.29.8
modernc.org/sqlite v1.20.3
)
require (
@ -48,11 +48,12 @@ require (
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/siphash v1.0.1 // indirect
github.com/andybalholm/brotli v1.0.3 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 // indirect
github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 // indirect
github.com/btcsuite/btcwallet/walletdb v1.4.2 // indirect
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 // indirect
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
github.com/btcsuite/btcwallet/walletdb v1.4.0 // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect
@ -63,84 +64,88 @@ require (
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/lru v1.1.2 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
github.com/decred/dcrd/lru v1.0.0 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/docker v24.0.9+incompatible // indirect
github.com/docker/docker v20.10.24+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fergusstrange/embedded-postgres v1.25.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/logr v1.2.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
github.com/jackc/pgx/v4 v4.18.2 // indirect
github.com/jackc/pgx/v4 v4.18.1 // indirect
github.com/jackpal/gateway v1.0.5 // indirect
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad // indirect
github.com/jonboulle/clockwork v0.2.2 // indirect
github.com/jrick/logrotate v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kkdai/bstream v1.0.0 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd // indirect
github.com/lightninglabs/neutrino/cache v1.1.2 // indirect
github.com/lightninglabs/neutrino v0.16.0 // indirect
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect
github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f // indirect
github.com/lightningnetwork/lnd/fn v1.0.5 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.4 // indirect
github.com/lightningnetwork/lnd/kvdb v1.4.8 // indirect
github.com/lightningnetwork/lnd/sqldb v1.0.2 // indirect
github.com/lightningnetwork/lnd/tlv v1.2.3 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.3 // indirect
github.com/lightningnetwork/lnd/kvdb v1.4.4 // indirect
github.com/lightningnetwork/lnd/tlv v1.1.1 // indirect
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mholt/archiver/v3 v3.5.0 // indirect
github.com/miekg/dns v1.1.43 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nwaples/rardecode v1.1.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.2 // indirect
github.com/opencontainers/runc v1.1.12 // indirect
github.com/opencontainers/runc v1.1.5 // indirect
github.com/pierrec/lz4/v4 v4.1.8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.11.1 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.2 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 // indirect
github.com/ulikunitz/xz v0.5.11 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
@ -155,47 +160,47 @@ require (
go.etcd.io/etcd/pkg/v3 v3.5.7 // indirect
go.etcd.io/etcd/raft/v3 v3.5.7 // indirect
go.etcd.io/etcd/server/v3 v3.5.7 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect
go.opentelemetry.io/otel v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.3.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
go.opentelemetry.io/otel/sdk v1.3.0 // indirect
go.opentelemetry.io/otel/trace v1.3.0 // indirect
go.opentelemetry.io/proto/otlp v0.15.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231030173426-d783a09b4405 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.1.0 // indirect
golang.org/x/tools v0.9.1 // indirect
google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.49.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)
// We want to format raw bytes as hex instead of base64. The forked version
// allows us to specify that as an option.
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display
replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display
replace github.com/lightninglabs/loop/swapserverrpc => ./swapserverrpc
go 1.22.3
go 1.19

777
go.sum

File diff suppressed because it is too large Load Diff

@ -1,634 +0,0 @@
package instantout
import (
"context"
"crypto/rand"
"errors"
"fmt"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
loop_rpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
)
const (
// Define route independent max routing fees. We have currently no way
// to get a reliable estimate of the routing fees. Best we can do is
// the minimum routing fees, which is not very indicative.
maxRoutingFeeBase = btcutil.Amount(10)
maxRoutingFeeRate = int64(20000)
// urgentConfTarget is the target number of blocks for the htlc to be
// confirmed quickly.
urgentConfTarget = int32(3)
// normalConfTarget is the target number of blocks for the sweepless
// sweep to be confirmed.
normalConfTarget = int32(6)
// defaultMaxParts is the default maximum number of parts for the swap.
defaultMaxParts = uint32(5)
// defaultSendpaymentTimeout is the default timeout for the swap invoice.
defaultSendpaymentTimeout = time.Minute * 5
// defaultPollPaymentTime is the default time to poll the server for the
// payment status.
defaultPollPaymentTime = time.Second * 15
// htlcExpiryDelta is the delta in blocks we require between the htlc
// expiry and reservation expiry.
htlcExpiryDelta = int32(40)
)
// InitInstantOutCtx contains the context for the InitInstantOutAction.
type InitInstantOutCtx struct {
cltvExpiry int32
reservations []reservation.ID
initationHeight int32
outgoingChanSet loopdb.ChannelSet
protocolVersion ProtocolVersion
sweepAddress btcutil.Address
}
// InitInstantOutAction is the first action that is executed when the instant
// out FSM is started. It will send the instant out request to the server.
func (f *FSM) InitInstantOutAction(eventCtx fsm.EventContext) fsm.EventType {
initCtx, ok := eventCtx.(*InitInstantOutCtx)
if !ok {
return f.HandleError(fsm.ErrInvalidContextType)
}
if len(initCtx.reservations) == 0 {
return f.HandleError(fmt.Errorf("no reservations provided"))
}
var (
reservationAmt uint64
reservationIds = make([][]byte, 0, len(initCtx.reservations))
reservations = make(
[]*reservation.Reservation, 0, len(initCtx.reservations),
)
)
// The requested amount needs to be full reservation amounts.
for _, reservationId := range initCtx.reservations {
resId := reservationId
res, err := f.cfg.ReservationManager.GetReservation(
f.ctx, resId,
)
if err != nil {
return f.HandleError(err)
}
// Check if the reservation is locked.
if res.State == reservation.Locked {
return f.HandleError(fmt.Errorf("reservation %v is "+
"locked", reservationId))
}
reservationAmt += uint64(res.Value)
reservationIds = append(reservationIds, resId[:])
reservations = append(reservations, res)
// Check that the reservation expiry is larger than the cltv
// expiry of the swap, with an additional delta to allow for
// preimage reveal.
if int32(res.Expiry) < initCtx.cltvExpiry+htlcExpiryDelta {
return f.HandleError(fmt.Errorf("reservation %x has "+
"expiry %v which is less than the swap expiry %v",
resId, res.Expiry, initCtx.cltvExpiry+htlcExpiryDelta))
}
}
// Create the preimage for the swap.
var preimage lntypes.Preimage
if _, err := rand.Read(preimage[:]); err != nil {
return f.HandleError(err)
}
// Create the keys for the swap.
keyRes, err := f.cfg.Wallet.DeriveNextKey(f.ctx, KeyFamily)
if err != nil {
return f.HandleError(err)
}
swapHash := preimage.Hash()
// Create a high fee rate so that the htlc will be confirmed quickly.
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, urgentConfTarget)
if err != nil {
f.Infof("error estimating fee rate: %v", err)
return f.HandleError(err)
}
// Send the instantout request to the server.
instantOutResponse, err := f.cfg.InstantOutClient.RequestInstantLoopOut(
f.ctx,
&loop_rpc.InstantLoopOutRequest{
ReceiverKey: keyRes.PubKey.SerializeCompressed(),
SwapHash: swapHash[:],
Expiry: initCtx.cltvExpiry,
HtlcFeeRate: uint64(feeRate),
ReservationIds: reservationIds,
ProtocolVersion: CurrentRpcProtocolVersion(),
},
)
if err != nil {
return f.HandleError(err)
}
// Decode the invoice to check if the hash is valid.
payReq, err := f.cfg.LndClient.DecodePaymentRequest(
f.ctx, instantOutResponse.SwapInvoice,
)
if err != nil {
return f.HandleError(err)
}
if swapHash != payReq.Hash {
return f.HandleError(fmt.Errorf("invalid swap invoice hash: "+
"expected %x got %x", preimage.Hash(), payReq.Hash))
}
serverPubkey, err := btcec.ParsePubKey(instantOutResponse.SenderKey)
if err != nil {
return f.HandleError(err)
}
// Create the address that we'll send the funds to.
sweepAddress := initCtx.sweepAddress
if sweepAddress == nil {
sweepAddress, err = f.cfg.Wallet.NextAddr(
f.ctx, lnwallet.DefaultAccountName,
walletrpc.AddressType_TAPROOT_PUBKEY, false,
)
if err != nil {
return f.HandleError(err)
}
}
// Now we can create the instant out.
instantOut := &InstantOut{
SwapHash: swapHash,
swapPreimage: preimage,
protocolVersion: ProtocolVersionFullReservation,
initiationHeight: initCtx.initationHeight,
outgoingChanSet: initCtx.outgoingChanSet,
CltvExpiry: initCtx.cltvExpiry,
clientPubkey: keyRes.PubKey,
serverPubkey: serverPubkey,
Value: btcutil.Amount(reservationAmt),
htlcFeeRate: feeRate,
swapInvoice: instantOutResponse.SwapInvoice,
Reservations: reservations,
keyLocator: keyRes.KeyLocator,
sweepAddress: sweepAddress,
}
err = f.cfg.Store.CreateInstantLoopOut(f.ctx, instantOut)
if err != nil {
return f.HandleError(err)
}
f.InstantOut = instantOut
return OnInit
}
// PollPaymentAcceptedAction locks the reservations, sends the payment to the
// server and polls the server for the payment status.
func (f *FSM) PollPaymentAcceptedAction(_ fsm.EventContext) fsm.EventType {
// Now that we're doing the swap, we first lock the reservations
// so that they can't be used for other swaps.
for _, reservation := range f.InstantOut.Reservations {
err := f.cfg.ReservationManager.LockReservation(
f.ctx, reservation.ID,
)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
}
// Now we send the payment to the server.
payChan, paymentErrChan, err := f.cfg.RouterClient.SendPayment(
f.ctx,
lndclient.SendPaymentRequest{
Invoice: f.InstantOut.swapInvoice,
Timeout: defaultSendpaymentTimeout,
MaxParts: defaultMaxParts,
MaxFee: getMaxRoutingFee(f.InstantOut.Value),
},
)
if err != nil {
f.Errorf("error sending payment: %v", err)
return f.handleErrorAndUnlockReservations(err)
}
// We'll continuously poll the server for the payment status.
pollPaymentTries := 0
// We want to poll quickly the first time.
timer := time.NewTimer(time.Second)
for {
select {
case payRes := <-payChan:
f.Debugf("payment result: %v", payRes)
if payRes.State == lnrpc.Payment_FAILED {
return f.handleErrorAndUnlockReservations(
fmt.Errorf("payment failed: %v",
payRes.FailureReason),
)
}
case err := <-paymentErrChan:
f.Errorf("error sending payment: %v", err)
return f.handleErrorAndUnlockReservations(err)
case <-f.ctx.Done():
return f.handleErrorAndUnlockReservations(nil)
case <-timer.C:
res, err := f.cfg.InstantOutClient.PollPaymentAccepted(
f.ctx, &loop_rpc.PollPaymentAcceptedRequest{
SwapHash: f.InstantOut.SwapHash[:],
},
)
if err != nil {
pollPaymentTries++
if pollPaymentTries > 20 {
return f.handleErrorAndUnlockReservations(err)
}
}
if res != nil && res.Accepted {
return OnPaymentAccepted
}
timer.Reset(defaultPollPaymentTime)
}
}
}
// BuildHTLCAction creates the htlc transaction, exchanges nonces with
// the server and sends the htlc signatures to the server.
func (f *FSM) BuildHTLCAction(eventCtx fsm.EventContext) fsm.EventType {
htlcSessions, htlcClientNonces, err := f.InstantOut.createMusig2Session(
f.ctx, f.cfg.Signer,
)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
f.htlcMusig2Sessions = htlcSessions
// Send the server the client nonces.
htlcInitRes, err := f.cfg.InstantOutClient.InitHtlcSig(
f.ctx,
&loop_rpc.InitHtlcSigRequest{
SwapHash: f.InstantOut.SwapHash[:],
HtlcClientNonces: htlcClientNonces,
},
)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
if len(htlcInitRes.HtlcServerNonces) != len(f.InstantOut.Reservations) {
return f.handleErrorAndUnlockReservations(
errors.New("invalid number of server nonces"),
)
}
htlcServerNonces, err := toNonces(htlcInitRes.HtlcServerNonces)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
// Now that our nonces are set, we can create and sign the htlc
// transaction.
htlcTx, err := f.InstantOut.createHtlcTransaction(f.cfg.Network)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
// Next we'll get our sweep tx signatures.
htlcSigs, err := f.InstantOut.signMusig2Tx(
f.ctx, f.cfg.Signer, htlcTx, f.htlcMusig2Sessions,
htlcServerNonces,
)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
// Send the server the htlc signatures.
htlcRes, err := f.cfg.InstantOutClient.PushHtlcSig(
f.ctx,
&loop_rpc.PushHtlcSigRequest{
SwapHash: f.InstantOut.SwapHash[:],
ClientSigs: htlcSigs,
},
)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
// We can now finalize the htlc transaction.
htlcTx, err = f.InstantOut.finalizeMusig2Transaction(
f.ctx, f.cfg.Signer, f.htlcMusig2Sessions, htlcTx,
htlcRes.ServerSigs,
)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
f.InstantOut.finalizedHtlcTx = htlcTx
return OnHtlcSigReceived
}
// PushPreimageAction pushes the preimage to the server. It also creates the
// sweepless sweep transaction and sends the signatures to the server. Finally,
// it publishes the sweepless sweep transaction. If any of the steps after
// pushing the preimage fail, the htlc timeout transaction will be published.
func (f *FSM) PushPreimageAction(eventCtx fsm.EventContext) fsm.EventType {
// First we'll create the musig2 context.
coopSessions, coopClientNonces, err := f.InstantOut.createMusig2Session(
f.ctx, f.cfg.Signer,
)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
f.sweeplessSweepSessions = coopSessions
// Get the feerate for the coop sweep.
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, normalConfTarget)
if err != nil {
return f.handleErrorAndUnlockReservations(err)
}
pushPreImageRes, err := f.cfg.InstantOutClient.PushPreimage(
f.ctx,
&loop_rpc.PushPreimageRequest{
Preimage: f.InstantOut.swapPreimage[:],
ClientNonces: coopClientNonces,
ClientSweepAddr: f.InstantOut.sweepAddress.String(),
MusigTxFeeRate: uint64(feeRate),
},
)
// Now that we have revealed the preimage, if any following step fail,
// we'll need to publish the htlc tx.
if err != nil {
f.LastActionError = err
return OnErrorPublishHtlc
}
// Now that we have the sweepless sweep signatures we can build and
// publish the sweepless sweep transaction.
sweepTx, err := f.InstantOut.createSweeplessSweepTx(feeRate)
if err != nil {
f.LastActionError = err
return OnErrorPublishHtlc
}
coopServerNonces, err := toNonces(pushPreImageRes.ServerNonces)
if err != nil {
f.LastActionError = err
return OnErrorPublishHtlc
}
// Next we'll get our sweep tx signatures.
_, err = f.InstantOut.signMusig2Tx(
f.ctx, f.cfg.Signer, sweepTx, f.sweeplessSweepSessions,
coopServerNonces,
)
if err != nil {
f.LastActionError = err
return OnErrorPublishHtlc
}
// Now we'll finalize the sweepless sweep transaction.
sweepTx, err = f.InstantOut.finalizeMusig2Transaction(
f.ctx, f.cfg.Signer, f.sweeplessSweepSessions, sweepTx,
pushPreImageRes.Musig2SweepSigs,
)
if err != nil {
f.LastActionError = err
return OnErrorPublishHtlc
}
txLabel := fmt.Sprintf("sweepless-sweep-%v",
f.InstantOut.swapPreimage.Hash())
// Publish the sweepless sweep transaction.
err = f.cfg.Wallet.PublishTransaction(f.ctx, sweepTx, txLabel)
if err != nil {
f.LastActionError = err
return OnErrorPublishHtlc
}
f.InstantOut.FinalizedSweeplessSweepTx = sweepTx
txHash := f.InstantOut.FinalizedSweeplessSweepTx.TxHash()
f.InstantOut.SweepTxHash = &txHash
return OnSweeplessSweepPublished
}
// WaitForSweeplessSweepConfirmedAction waits for the sweepless sweep
// transaction to be confirmed.
func (f *FSM) WaitForSweeplessSweepConfirmedAction(
eventCtx fsm.EventContext) fsm.EventType {
pkscript, err := txscript.PayToAddrScript(f.InstantOut.sweepAddress)
if err != nil {
return f.HandleError(err)
}
confChan, confErrChan, err := f.cfg.ChainNotifier.
RegisterConfirmationsNtfn(
f.ctx, f.InstantOut.SweepTxHash, pkscript,
1, f.InstantOut.initiationHeight,
)
if err != nil {
return f.HandleError(err)
}
for {
select {
case spendErr := <-confErrChan:
f.LastActionError = spendErr
f.Errorf("error listening for sweepless sweep "+
"confirmation: %v", spendErr)
return OnErrorPublishHtlc
case conf := <-confChan:
f.InstantOut.
sweepConfirmationHeight = conf.BlockHeight
return OnSweeplessSweepConfirmed
}
}
}
// PublishHtlcAction publishes the htlc transaction and the htlc sweep
// transaction.
func (f *FSM) PublishHtlcAction(eventCtx fsm.EventContext) fsm.EventType {
// Publish the htlc transaction.
err := f.cfg.Wallet.PublishTransaction(
f.ctx, f.InstantOut.finalizedHtlcTx,
fmt.Sprintf("htlc-%v", f.InstantOut.swapPreimage.Hash()),
)
if err != nil {
return f.HandleError(err)
}
txHash := f.InstantOut.finalizedHtlcTx.TxHash()
f.Debugf("published htlc tx: %v", txHash)
// We'll now wait for the htlc to be confirmed.
confChan, confErrChan, err := f.cfg.ChainNotifier.
RegisterConfirmationsNtfn(
f.ctx, &txHash,
f.InstantOut.finalizedHtlcTx.TxOut[0].PkScript,
1, f.InstantOut.initiationHeight,
)
if err != nil {
return f.HandleError(err)
}
for {
select {
case spendErr := <-confErrChan:
return f.HandleError(spendErr)
case <-confChan:
return OnHtlcPublished
}
}
}
// PublishHtlcSweepAction publishes the htlc sweep transaction.
func (f *FSM) PublishHtlcSweepAction(eventCtx fsm.EventContext) fsm.EventType {
// Create a feerate that will confirm the htlc quickly.
feeRate, err := f.cfg.Wallet.EstimateFeeRate(f.ctx, urgentConfTarget)
if err != nil {
return f.HandleError(err)
}
getInfo, err := f.cfg.LndClient.GetInfo(f.ctx)
if err != nil {
return f.HandleError(err)
}
// We can immediately publish the htlc sweep transaction.
htlcSweepTx, err := f.InstantOut.generateHtlcSweepTx(
f.ctx, f.cfg.Signer, feeRate, f.cfg.Network, getInfo.BlockHeight,
)
if err != nil {
return f.HandleError(err)
}
label := fmt.Sprintf("htlc-sweep-%v", f.InstantOut.swapPreimage.Hash())
err = f.cfg.Wallet.PublishTransaction(f.ctx, htlcSweepTx, label)
if err != nil {
log.Errorf("error publishing htlc sweep tx: %v", err)
return f.HandleError(err)
}
sweepTxHash := htlcSweepTx.TxHash()
f.InstantOut.SweepTxHash = &sweepTxHash
return OnHtlcSweepPublished
}
// WaitForHtlcSweepConfirmedAction waits for the htlc sweep transaction to be
// confirmed.
func (f *FSM) WaitForHtlcSweepConfirmedAction(
eventCtx fsm.EventContext) fsm.EventType {
sweepPkScript, err := txscript.PayToAddrScript(
f.InstantOut.sweepAddress,
)
if err != nil {
return f.HandleError(err)
}
confChan, confErrChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn(
f.ctx, f.InstantOut.SweepTxHash, sweepPkScript,
1, f.InstantOut.initiationHeight,
)
if err != nil {
return f.HandleError(err)
}
f.Debugf("waiting for htlc sweep tx %v to be confirmed",
f.InstantOut.SweepTxHash)
for {
select {
case spendErr := <-confErrChan:
return f.HandleError(spendErr)
case conf := <-confChan:
f.InstantOut.
sweepConfirmationHeight = conf.BlockHeight
return OnHtlcSwept
}
}
}
// handleErrorAndUnlockReservations handles an error and unlocks the
// reservations.
func (f *FSM) handleErrorAndUnlockReservations(err error) fsm.EventType {
// We might get here from a canceled context, we create a new context
// with a timeout to unlock the reservations.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
// Unlock the reservations.
for _, reservation := range f.InstantOut.Reservations {
err := f.cfg.ReservationManager.UnlockReservation(
ctx, reservation.ID,
)
if err != nil {
f.Errorf("error unlocking reservation: %v", err)
return f.HandleError(err)
}
}
// We're also sending the server a cancel message so that it can
// release the reservations. This can be done in a goroutine as we
// wan't to fail the fsm early.
go func() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
_, cancelErr := f.cfg.InstantOutClient.CancelInstantSwap(
ctx, &loop_rpc.CancelInstantSwapRequest{
SwapHash: f.InstantOut.SwapHash[:],
},
)
if cancelErr != nil {
// We'll log the error but not return it as we want to return the
// original error.
f.Debugf("error sending cancel message: %v", cancelErr)
}
}()
return f.HandleError(err)
}
func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
return swap.CalcFee(amt, maxRoutingFeeBase, maxRoutingFeeRate)
}

@ -1,401 +0,0 @@
package instantout
import (
"context"
"errors"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
loop_rpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/input"
)
type ProtocolVersion uint32
const (
// ProtocolVersionUndefined is the undefined protocol version.
ProtocolVersionUndefined ProtocolVersion = 0
// ProtocolVersionFullReservation is the protocol version that uses
// the full reservation amount without change.
ProtocolVersionFullReservation ProtocolVersion = 1
)
// CurrentProtocolVersion returns the current protocol version.
func CurrentProtocolVersion() ProtocolVersion {
return ProtocolVersionFullReservation
}
// CurrentRpcProtocolVersion returns the current rpc protocol version.
func CurrentRpcProtocolVersion() loop_rpc.InstantOutProtocolVersion {
return loop_rpc.InstantOutProtocolVersion(CurrentProtocolVersion())
}
const (
// defaultObserverSize is the size of the fsm observer channel.
defaultObserverSize = 15
)
var (
ErrProtocolVersionNotSupported = errors.New(
"protocol version not supported",
)
)
// States.
var (
// Init is the initial state of the instant out FSM.
Init = fsm.StateType("Init")
// SendPaymentAndPollAccepted is the state where the payment is sent
// and the server is polled for the accepted state.
SendPaymentAndPollAccepted = fsm.StateType("SendPaymentAndPollAccepted")
// BuildHtlc is the state where the htlc transaction is built.
BuildHtlc = fsm.StateType("BuildHtlc")
// PushPreimage is the state where the preimage is pushed to the server.
PushPreimage = fsm.StateType("PushPreimage")
// WaitForSweeplessSweepConfirmed is the state where we wait for the
// sweepless sweep to be confirmed.
WaitForSweeplessSweepConfirmed = fsm.StateType(
"WaitForSweeplessSweepConfirmed")
// FinishedSweeplessSweep is the state where the swap is finished by
// publishing the sweepless sweep.
FinishedSweeplessSweep = fsm.StateType("FinishedSweeplessSweep")
// PublishHtlc is the state where the htlc transaction is published.
PublishHtlc = fsm.StateType("PublishHtlc")
// PublishHtlcSweep is the state where the htlc sweep transaction is
// published.
PublishHtlcSweep = fsm.StateType("PublishHtlcSweep")
// FinishedHtlcPreimageSweep is the state where the swap is finished by
// publishing the htlc preimage sweep.
FinishedHtlcPreimageSweep = fsm.StateType("FinishedHtlcPreimageSweep")
// WaitForHtlcSweepConfirmed is the state where we wait for the htlc
// sweep to be confirmed.
WaitForHtlcSweepConfirmed = fsm.StateType("WaitForHtlcSweepConfirmed")
// FailedHtlcSweep is the state where the htlc sweep failed.
FailedHtlcSweep = fsm.StateType("FailedHtlcSweep")
// Failed is the state where the swap failed.
Failed = fsm.StateType("InstantOutFailed")
)
// Events.
var (
// OnStart is the event that is sent when the FSM is started.
OnStart = fsm.EventType("OnStart")
// OnInit is the event that is triggered when the FSM is initialized.
OnInit = fsm.EventType("OnInit")
// OnPaymentAccepted is the event that is triggered when the payment
// is accepted by the server.
OnPaymentAccepted = fsm.EventType("OnPaymentAccepted")
// OnHtlcSigReceived is the event that is triggered when the htlc sig
// is received.
OnHtlcSigReceived = fsm.EventType("OnHtlcSigReceived")
// OnPreimagePushed is the event that is triggered when the preimage
// is pushed to the server.
OnPreimagePushed = fsm.EventType("OnPreimagePushed")
// OnSweeplessSweepPublished is the event that is triggered when the
// sweepless sweep is published.
OnSweeplessSweepPublished = fsm.EventType("OnSweeplessSweepPublished")
// OnSweeplessSweepConfirmed is the event that is triggered when the
// sweepless sweep is confirmed.
OnSweeplessSweepConfirmed = fsm.EventType("OnSweeplessSweepConfirmed")
// OnErrorPublishHtlc is the event that is triggered when the htlc
// sweep is published after an error.
OnErrorPublishHtlc = fsm.EventType("OnErrorPublishHtlc")
// OnInvalidCoopSweep is the event that is triggered when the coop
// sweep is invalid.
OnInvalidCoopSweep = fsm.EventType("OnInvalidCoopSweep")
// OnHtlcPublished is the event that is triggered when the htlc
// transaction is published.
OnHtlcPublished = fsm.EventType("OnHtlcPublished")
// OnHtlcSweepPublished is the event that is triggered when the htlc
// sweep is published.
OnHtlcSweepPublished = fsm.EventType("OnHtlcSweepPublished")
// OnHtlcSwept is the event that is triggered when the htlc sweep is
// confirmed.
OnHtlcSwept = fsm.EventType("OnHtlcSwept")
// OnRecover is the event that is triggered when the FSM recovers from
// a restart.
OnRecover = fsm.EventType("OnRecover")
)
// Config contains the services required for the instant out FSM.
type Config struct {
// Store is used to store the instant out.
Store InstantLoopOutStore
// LndClient is used to decode the swap invoice.
LndClient lndclient.LightningClient
// RouterClient is used to send the offchain payment to the server.
RouterClient lndclient.RouterClient
// ChainNotifier is used to be notified of on-chain events.
ChainNotifier lndclient.ChainNotifierClient
// Signer is used to sign transactions.
Signer lndclient.SignerClient
// Wallet is used to derive keys.
Wallet lndclient.WalletKitClient
// InstantOutClient is used to communicate with the swap server.
InstantOutClient loop_rpc.InstantSwapServerClient
// ReservationManager is used to get the reservations and lock them.
ReservationManager ReservationManager
// Network is the network that is used for the swap.
Network *chaincfg.Params
}
// FSM is the state machine that handles the instant out.
type FSM struct {
*fsm.StateMachine
ctx context.Context
// cfg contains all the services that the reservation manager needs to
// operate.
cfg *Config
// InstantOut contains all the information about the instant out.
InstantOut *InstantOut
// htlcMusig2Sessions contains all the reservations input musig2
// sessions that will be used for the htlc transaction.
htlcMusig2Sessions []*input.MuSig2SessionInfo
// sweeplessSweepSessions contains all the reservations input musig2
// sessions that will be used for the sweepless sweep transaction.
sweeplessSweepSessions []*input.MuSig2SessionInfo
}
// NewFSM creates a new instant out FSM.
func NewFSM(ctx context.Context, cfg *Config,
protocolVersion ProtocolVersion) (*FSM, error) {
instantOut := &InstantOut{
State: fsm.EmptyState,
protocolVersion: protocolVersion,
}
return NewFSMFromInstantOut(ctx, cfg, instantOut)
}
// NewFSMFromInstantOut creates a new instantout FSM from an existing instantout
// recovered from the database.
func NewFSMFromInstantOut(ctx context.Context, cfg *Config,
instantOut *InstantOut) (*FSM, error) {
instantOutFSM := &FSM{
ctx: ctx,
cfg: cfg,
InstantOut: instantOut,
}
switch instantOut.protocolVersion {
case ProtocolVersionFullReservation:
instantOutFSM.StateMachine = fsm.NewStateMachineWithState(
instantOutFSM.GetV1ReservationStates(),
instantOut.State, defaultObserverSize,
)
default:
return nil, ErrProtocolVersionNotSupported
}
instantOutFSM.ActionEntryFunc = instantOutFSM.updateInstantOut
return instantOutFSM, nil
}
// GetV1ReservationStates returns the states for the v1 reservation.
func (f *FSM) GetV1ReservationStates() fsm.States {
return fsm.States{
fsm.EmptyState: fsm.State{
Transitions: fsm.Transitions{
OnStart: Init,
},
Action: nil,
},
Init: fsm.State{
Transitions: fsm.Transitions{
OnInit: SendPaymentAndPollAccepted,
fsm.OnError: Failed,
OnRecover: Failed,
},
Action: f.InitInstantOutAction,
},
SendPaymentAndPollAccepted: fsm.State{
Transitions: fsm.Transitions{
OnPaymentAccepted: BuildHtlc,
fsm.OnError: Failed,
OnRecover: Failed,
},
Action: f.PollPaymentAcceptedAction,
},
BuildHtlc: fsm.State{
Transitions: fsm.Transitions{
OnHtlcSigReceived: PushPreimage,
fsm.OnError: Failed,
OnRecover: Failed,
},
Action: f.BuildHTLCAction,
},
PushPreimage: fsm.State{
Transitions: fsm.Transitions{
OnSweeplessSweepPublished: WaitForSweeplessSweepConfirmed,
fsm.OnError: Failed,
OnErrorPublishHtlc: PublishHtlc,
OnRecover: PushPreimage,
},
Action: f.PushPreimageAction,
},
WaitForSweeplessSweepConfirmed: fsm.State{
Transitions: fsm.Transitions{
OnSweeplessSweepConfirmed: FinishedSweeplessSweep,
OnRecover: WaitForSweeplessSweepConfirmed,
fsm.OnError: PublishHtlc,
},
Action: f.WaitForSweeplessSweepConfirmedAction,
},
FinishedSweeplessSweep: fsm.State{
Transitions: fsm.Transitions{},
Action: fsm.NoOpAction,
},
PublishHtlc: fsm.State{
Transitions: fsm.Transitions{
fsm.OnError: FailedHtlcSweep,
OnRecover: PublishHtlc,
OnHtlcPublished: PublishHtlcSweep,
},
Action: f.PublishHtlcAction,
},
PublishHtlcSweep: fsm.State{
Transitions: fsm.Transitions{
OnHtlcSweepPublished: WaitForHtlcSweepConfirmed,
OnRecover: PublishHtlcSweep,
fsm.OnError: FailedHtlcSweep,
},
Action: f.PublishHtlcSweepAction,
},
WaitForHtlcSweepConfirmed: fsm.State{
Transitions: fsm.Transitions{
OnHtlcSwept: FinishedHtlcPreimageSweep,
OnRecover: WaitForHtlcSweepConfirmed,
fsm.OnError: FailedHtlcSweep,
},
Action: f.WaitForHtlcSweepConfirmedAction,
},
FinishedHtlcPreimageSweep: fsm.State{
Transitions: fsm.Transitions{},
Action: fsm.NoOpAction,
},
FailedHtlcSweep: fsm.State{
Action: fsm.NoOpAction,
Transitions: fsm.Transitions{
OnRecover: PublishHtlcSweep,
},
},
Failed: fsm.State{
Action: fsm.NoOpAction,
},
}
}
// updateInstantOut is called after every action and updates the reservation
// in the db.
func (f *FSM) updateInstantOut(notification fsm.Notification) {
f.Infof("Previous: %v, Event: %v, Next: %v", notification.PreviousState,
notification.Event, notification.NextState)
// Skip the update if the reservation is not yet initialized.
if f.InstantOut == nil {
return
}
f.InstantOut.State = notification.NextState
// If we're in the early stages we don't have created the reservation
// in the store yet and won't need to update it.
if f.InstantOut.State == Init ||
f.InstantOut.State == fsm.EmptyState ||
(notification.PreviousState == Init &&
f.InstantOut.State == Failed) {
return
}
err := f.cfg.Store.UpdateInstantLoopOut(f.ctx, f.InstantOut)
if err != nil {
log.Errorf("Error updating instant out: %v", err)
return
}
}
// Infof logs an info message with the reservation hash as prefix.
func (f *FSM) Infof(format string, args ...interface{}) {
log.Infof(
"InstantOut %v: "+format,
append(
[]interface{}{f.InstantOut.swapPreimage.Hash()},
args...,
)...,
)
}
// Debugf logs a debug message with the reservation hash as prefix.
func (f *FSM) Debugf(format string, args ...interface{}) {
log.Debugf(
"InstantOut %v: "+format,
append(
[]interface{}{f.InstantOut.swapPreimage.Hash()},
args...,
)...,
)
}
// Errorf logs an error message with the reservation hash as prefix.
func (f *FSM) Errorf(format string, args ...interface{}) {
log.Errorf(
"InstantOut %v: "+format,
append(
[]interface{}{f.InstantOut.swapPreimage.Hash()},
args...,
)...,
)
}
// isFinalState returns true if the state is a final state.
func isFinalState(state fsm.StateType) bool {
switch state {
case Failed, FinishedHtlcPreimageSweep,
FinishedSweeplessSweep:
return true
}
return false
}

@ -1,36 +0,0 @@
```mermaid
stateDiagram-v2
[*] --> Init: OnStart
BuildHtlc
BuildHtlc --> PushPreimage: OnHtlcSigReceived
BuildHtlc --> InstantFailedOutFailed: OnError
BuildHtlc --> InstantFailedOutFailed: OnRecover
FailedHtlcSweep
FinishedSweeplessSweep
Init
Init --> SendPaymentAndPollAccepted: OnInit
Init --> InstantFailedOutFailed: OnError
Init --> InstantFailedOutFailed: OnRecover
InstantFailedOutFailed
PublishHtlc
PublishHtlc --> FailedHtlcSweep: OnError
PublishHtlc --> PublishHtlc: OnRecover
PublishHtlc --> WaitForHtlcSweepConfirmed: OnHtlcBroadcasted
PushPreimage
PushPreimage --> PushPreimage: OnRecover
PushPreimage --> WaitForSweeplessSweepConfirmed: OnSweeplessSweepPublished
PushPreimage --> InstantFailedOutFailed: OnError
PushPreimage --> PublishHtlc: OnErrorPublishHtlc
SendPaymentAndPollAccepted
SendPaymentAndPollAccepted --> BuildHtlc: OnPaymentAccepted
SendPaymentAndPollAccepted --> InstantFailedOutFailed: OnError
SendPaymentAndPollAccepted --> InstantFailedOutFailed: OnRecover
WaitForHtlcSweepConfirmed
WaitForHtlcSweepConfirmed --> FinishedHtlcPreimageSweep: OnHtlcSwept
WaitForHtlcSweepConfirmed --> WaitForHtlcSweepConfirmed: OnRecover
WaitForHtlcSweepConfirmed --> FailedHtlcSweep: OnError
WaitForSweeplessSweepConfirmed
WaitForSweeplessSweepConfirmed --> FinishedSweeplessSweep: OnSweeplessSweepConfirmed
WaitForSweeplessSweepConfirmed --> WaitForSweeplessSweepConfirmed: OnRecover
WaitForSweeplessSweepConfirmed --> PublishHtlc: OnError
```

@ -1,488 +0,0 @@
package instantout
import (
"context"
"errors"
"fmt"
"reflect"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
// InstantOut holds the necessary information to execute an instant out swap.
type InstantOut struct {
// SwapHash is the hash of the swap.
SwapHash lntypes.Hash
// swapPreimage is the preimage that is used for the swap.
swapPreimage lntypes.Preimage
// State is the current state of the swap.
State fsm.StateType
// CltvExpiry is the expiry of the swap.
CltvExpiry int32
// outgoingChanSet optionally specifies the short channel ids of the
// channels that may be used to loop out.
outgoingChanSet loopdb.ChannelSet
// Reservations are the Reservations that are used in as inputs for the
// instant out swap.
Reservations []*reservation.Reservation
// protocolVersion is the version of the protocol that is used for the
// swap.
protocolVersion ProtocolVersion
// initiationHeight is the height at which the swap was initiated.
initiationHeight int32
// Value is the amount that is swapped.
Value btcutil.Amount
// keyLocator is the key locator that is used for the swap.
keyLocator keychain.KeyLocator
// clientPubkey is the pubkey of the client that is used for the swap.
clientPubkey *btcec.PublicKey
// serverPubkey is the pubkey of the server that is used for the swap.
serverPubkey *btcec.PublicKey
// swapInvoice is the invoice that is used for the swap.
swapInvoice string
// htlcFeeRate is the fee rate that is used for the htlc transaction.
htlcFeeRate chainfee.SatPerKWeight
// sweepAddress is the address that is used to sweep the funds to.
sweepAddress btcutil.Address
// finalizedHtlcTx is the finalized htlc transaction that is used in the
// non-cooperative path for the instant out swap.
finalizedHtlcTx *wire.MsgTx
// SweepTxHash is the hash of the sweep transaction.
SweepTxHash *chainhash.Hash
// FinalizedSweeplessSweepTx is the transaction that is used to sweep
// the funds in the cooperative path.
FinalizedSweeplessSweepTx *wire.MsgTx
// sweepConfirmationHeight is the height at which the sweep
// transaction was confirmed.
sweepConfirmationHeight uint32
}
// getHtlc returns the swap.htlc for the instant out.
func (i *InstantOut) getHtlc(chainParams *chaincfg.Params) (*swap.Htlc, error) {
return swap.NewHtlcV2(
i.CltvExpiry, pubkeyTo33ByteSlice(i.serverPubkey),
pubkeyTo33ByteSlice(i.clientPubkey), i.SwapHash, chainParams,
)
}
// createMusig2Session creates a musig2 session for the instant out.
func (i *InstantOut) createMusig2Session(ctx context.Context,
signer lndclient.SignerClient) ([]*input.MuSig2SessionInfo,
[][]byte, error) {
// Create the htlc musig2 context.
musig2Sessions := make([]*input.MuSig2SessionInfo, len(i.Reservations))
clientNonces := make([][]byte, len(i.Reservations))
// Create the sessions and nonces from the reservations.
for idx, reservation := range i.Reservations {
session, err := reservation.Musig2CreateSession(ctx, signer)
if err != nil {
return nil, nil, err
}
musig2Sessions[idx] = session
clientNonces[idx] = session.PublicNonce[:]
}
return musig2Sessions, clientNonces, nil
}
// getInputReservation returns the input reservation for the instant out.
func (i *InstantOut) getInputReservations() (InputReservations, error) {
if len(i.Reservations) == 0 {
return nil, errors.New("no reservations")
}
inputs := make(InputReservations, len(i.Reservations))
for idx, reservation := range i.Reservations {
pkScript, err := reservation.GetPkScript()
if err != nil {
return nil, err
}
inputs[idx] = InputReservation{
Outpoint: *reservation.Outpoint,
Value: reservation.Value,
PkScript: pkScript,
}
}
return inputs, nil
}
// createHtlcTransaction creates the htlc transaction for the instant out.
func (i *InstantOut) createHtlcTransaction(network *chaincfg.Params) (
*wire.MsgTx, error) {
if network == nil {
return nil, errors.New("no network provided")
}
inputReservations, err := i.getInputReservations()
if err != nil {
return nil, err
}
// First Create the tx.
msgTx := wire.NewMsgTx(2)
// add the reservation inputs
for _, reservation := range inputReservations {
msgTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: reservation.Outpoint,
})
}
// Estimate the fee
weight := htlcWeight(len(inputReservations))
fee := i.htlcFeeRate.FeeForWeight(weight)
if fee > i.Value/5 {
return nil, errors.New("fee is higher than 20% of " +
"sweep value")
}
htlc, err := i.getHtlc(network)
if err != nil {
return nil, err
}
// Create the sweep output
sweepOutput := &wire.TxOut{
Value: int64(i.Value) - int64(fee),
PkScript: htlc.PkScript,
}
msgTx.AddTxOut(sweepOutput)
return msgTx, nil
}
// createSweeplessSweepTx creates the sweepless sweep transaction for the
// instant out.
func (i *InstantOut) createSweeplessSweepTx(feerate chainfee.SatPerKWeight) (
*wire.MsgTx, error) {
inputReservations, err := i.getInputReservations()
if err != nil {
return nil, err
}
// First Create the tx.
msgTx := wire.NewMsgTx(2)
// add the reservation inputs
for _, reservation := range inputReservations {
msgTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: reservation.Outpoint,
})
}
// Estimate the fee
weight := sweeplessSweepWeight(len(inputReservations))
fee := feerate.FeeForWeight(weight)
if fee > i.Value/5 {
return nil, errors.New("fee is higher than 20% of " +
"sweep value")
}
pkscript, err := txscript.PayToAddrScript(i.sweepAddress)
if err != nil {
return nil, err
}
// Create the sweep output
sweepOutput := &wire.TxOut{
Value: int64(i.Value) - int64(fee),
PkScript: pkscript,
}
msgTx.AddTxOut(sweepOutput)
return msgTx, nil
}
// signMusig2Tx adds the server nonces to the musig2 sessions and signs the
// transaction.
func (i *InstantOut) signMusig2Tx(ctx context.Context,
signer lndclient.SignerClient, tx *wire.MsgTx,
musig2sessions []*input.MuSig2SessionInfo,
counterPartyNonces [][66]byte) ([][]byte, error) {
inputs, err := i.getInputReservations()
if err != nil {
return nil, err
}
prevOutFetcher := inputs.GetPrevoutFetcher()
sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher)
sigs := make([][]byte, len(inputs))
for idx, reservation := range inputs {
if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint,
reservation.Outpoint) {
return nil, fmt.Errorf("tx input does not match " +
"reservation")
}
taprootSigHash, err := txscript.CalcTaprootSignatureHash(
sigHashes, txscript.SigHashDefault,
tx, idx, prevOutFetcher,
)
if err != nil {
return nil, err
}
var digest [32]byte
copy(digest[:], taprootSigHash)
// Register the server's nonce before attempting to create our
// partial signature.
haveAllNonces, err := signer.MuSig2RegisterNonces(
ctx, musig2sessions[idx].SessionID,
[][musig2.PubNonceSize]byte{counterPartyNonces[idx]},
)
if err != nil {
return nil, err
}
// Sanity check that we have all the nonces.
if !haveAllNonces {
return nil, fmt.Errorf("invalid MuSig2 session: " +
"nonces missing")
}
// Since our MuSig2 session has all nonces, we can now create
// the local partial signature by signing the sig hash.
sig, err := signer.MuSig2Sign(
ctx, musig2sessions[idx].SessionID, digest, false,
)
if err != nil {
return nil, err
}
sigs[idx] = sig
}
return sigs, nil
}
// finalizeMusig2Transaction creates the finalized transactions for either
// the htlc or the cooperative close.
func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context,
signer lndclient.SignerClient,
musig2Sessions []*input.MuSig2SessionInfo,
tx *wire.MsgTx, serverSigs [][]byte) (*wire.MsgTx, error) {
inputs, err := i.getInputReservations()
if err != nil {
return nil, err
}
for idx := range inputs {
haveAllSigs, finalSig, err := signer.MuSig2CombineSig(
ctx, musig2Sessions[idx].SessionID,
[][]byte{serverSigs[idx]},
)
if err != nil {
return nil, err
}
if !haveAllSigs {
return nil, fmt.Errorf("missing sigs")
}
tx.TxIn[idx].Witness = wire.TxWitness{finalSig}
}
return tx, nil
}
// generateHtlcSweepTx creates the htlc sweep transaction for the instant out.
func (i *InstantOut) generateHtlcSweepTx(ctx context.Context,
signer lndclient.SignerClient, feeRate chainfee.SatPerKWeight,
network *chaincfg.Params, blockheight uint32) (
*wire.MsgTx, error) {
if network == nil {
return nil, errors.New("no network provided")
}
if i.finalizedHtlcTx == nil {
return nil, errors.New("no finalized htlc tx")
}
htlc, err := i.getHtlc(network)
if err != nil {
return nil, err
}
// Create the sweep transaction.
sweepTx := wire.NewMsgTx(2)
sweepTx.LockTime = blockheight
var weightEstimator input.TxWeightEstimator
weightEstimator.AddP2TROutput()
err = htlc.AddSuccessToEstimator(&weightEstimator)
if err != nil {
return nil, err
}
htlcHash := i.finalizedHtlcTx.TxHash()
// Add the htlc input.
sweepTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: htlcHash,
Index: 0,
},
SignatureScript: htlc.SigScript,
Sequence: htlc.SuccessSequence(),
})
// Add the sweep output.
sweepPkScript, err := txscript.PayToAddrScript(i.sweepAddress)
if err != nil {
return nil, err
}
fee := feeRate.FeeForWeight(weightEstimator.Weight())
htlcOutValue := i.finalizedHtlcTx.TxOut[0].Value
output := &wire.TxOut{
Value: htlcOutValue - int64(fee),
PkScript: sweepPkScript,
}
sweepTx.AddTxOut(output)
signDesc := lndclient.SignDescriptor{
WitnessScript: htlc.SuccessScript(),
Output: &wire.TxOut{
Value: htlcOutValue,
PkScript: htlc.PkScript,
},
HashType: htlc.SigHash(),
InputIndex: 0,
KeyDesc: keychain.KeyDescriptor{
KeyLocator: i.keyLocator,
},
}
rawSigs, err := signer.SignOutputRaw(
ctx, sweepTx, []*lndclient.SignDescriptor{&signDesc}, nil,
)
if err != nil {
return nil, fmt.Errorf("sign output error: %v", err)
}
sig := rawSigs[0]
// Add witness stack to the tx input.
sweepTx.TxIn[0].Witness, err = htlc.GenSuccessWitness(
sig, i.swapPreimage,
)
if err != nil {
return nil, err
}
return sweepTx, nil
}
// htlcWeight returns the weight for the htlc transaction.
func htlcWeight(numInputs int) lntypes.WeightUnit {
var weightEstimator input.TxWeightEstimator
for i := 0; i < numInputs; i++ {
weightEstimator.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
}
weightEstimator.AddP2WSHOutput()
return weightEstimator.Weight()
}
// sweeplessSweepWeight returns the weight for the sweepless sweep transaction.
func sweeplessSweepWeight(numInputs int) lntypes.WeightUnit {
var weightEstimator input.TxWeightEstimator
for i := 0; i < numInputs; i++ {
weightEstimator.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
}
weightEstimator.AddP2TROutput()
return weightEstimator.Weight()
}
// pubkeyTo33ByteSlice converts a pubkey to a 33 byte slice.
func pubkeyTo33ByteSlice(pubkey *btcec.PublicKey) [33]byte {
var pubkeyBytes [33]byte
copy(pubkeyBytes[:], pubkey.SerializeCompressed())
return pubkeyBytes
}
// toNonces converts a byte slice to a 66 byte slice.
func toNonces(nonces [][]byte) ([][66]byte, error) {
res := make([][66]byte, 0, len(nonces))
for _, n := range nonces {
n := n
nonce, err := byteSliceTo66ByteSlice(n)
if err != nil {
return nil, err
}
res = append(res, nonce)
}
return res, nil
}
// byteSliceTo66ByteSlice converts a byte slice to a 66 byte slice.
func byteSliceTo66ByteSlice(b []byte) ([66]byte, error) {
if len(b) != 66 {
return [66]byte{}, fmt.Errorf("invalid byte slice length")
}
var res [66]byte
copy(res[:], b)
return res, nil
}

@ -1,73 +0,0 @@
package instantout
import (
"context"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/instantout/reservation"
)
const (
KeyFamily = int32(42069)
)
// InstantLoopOutStore is the interface that needs to be implemented by a
// store that wants to be used by the instant loop out manager.
type InstantLoopOutStore interface {
// CreateInstantLoopOut adds a new instant loop out to the store.
CreateInstantLoopOut(ctx context.Context, instantOut *InstantOut) error
// UpdateInstantLoopOut updates an existing instant loop out in the
// store.
UpdateInstantLoopOut(ctx context.Context, instantOut *InstantOut) error
// GetInstantLoopOut returns the instant loop out for the given swap
// hash.
GetInstantLoopOut(ctx context.Context, swapHash []byte) (*InstantOut, error)
// ListInstantLoopOuts returns all instant loop outs that are in the
// store.
ListInstantLoopOuts(ctx context.Context) ([]*InstantOut, error)
}
// ReservationManager handles fetching and locking of reservations.
type ReservationManager interface {
// GetReservation returns the reservation for the given id.
GetReservation(ctx context.Context, id reservation.ID) (
*reservation.Reservation, error)
// LockReservation locks the reservation for the given id.
LockReservation(ctx context.Context, id reservation.ID) error
// UnlockReservation unlocks the reservation for the given id.
UnlockReservation(ctx context.Context, id reservation.ID) error
}
// InputReservations is a helper struct for the input reservations.
type InputReservations []InputReservation
// InputReservation is a helper struct for the input reservation.
type InputReservation struct {
Outpoint wire.OutPoint
Value btcutil.Amount
PkScript []byte
}
// Output returns the output for the input reservation.
func (r InputReservation) Output() *wire.TxOut {
return wire.NewTxOut(int64(r.Value), r.PkScript)
}
// GetPrevoutFetcher returns a prevout fetcher for the input reservations.
func (i InputReservations) GetPrevoutFetcher() txscript.PrevOutputFetcher {
prevOuts := make(map[wire.OutPoint]*wire.TxOut)
// add the reservation inputs
for _, reservation := range i {
prevOuts[reservation.Outpoint] = reservation.Output()
}
return txscript.NewMultiPrevOutFetcher(prevOuts)
}

@ -1,26 +0,0 @@
package instantout
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Subsystem defines the sub system name of this package.
const Subsystem = "INST"
// 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
}

@ -1,265 +0,0 @@
package instantout
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout/reservation"
looprpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
defaultStateWaitTime = 30 * time.Second
defaultCltv = 100
ErrSwapDoesNotExist = errors.New("swap does not exist")
)
// Manager manages the instantout state machines.
type Manager struct {
// cfg contains all the services that the reservation manager needs to
// operate.
cfg *Config
// activeInstantOuts contains all the active instantouts.
activeInstantOuts map[lntypes.Hash]*FSM
// currentHeight stores the currently best known block height.
currentHeight int32
// blockEpochChan receives new block heights.
blockEpochChan chan int32
runCtx context.Context
sync.Mutex
}
// NewInstantOutManager creates a new instantout manager.
func NewInstantOutManager(cfg *Config) *Manager {
return &Manager{
cfg: cfg,
activeInstantOuts: make(map[lntypes.Hash]*FSM),
blockEpochChan: make(chan int32),
}
}
// Run runs the instantout manager.
func (m *Manager) Run(ctx context.Context, initChan chan struct{},
height int32) error {
log.Debugf("Starting instantout manager")
defer func() {
log.Debugf("Stopping instantout manager")
}()
runCtx, cancel := context.WithCancel(ctx)
defer cancel()
m.runCtx = runCtx
m.currentHeight = height
err := m.recoverInstantOuts(runCtx)
if err != nil {
close(initChan)
return err
}
newBlockChan, newBlockErrChan, err := m.cfg.ChainNotifier.
RegisterBlockEpochNtfn(ctx)
if err != nil {
close(initChan)
return err
}
close(initChan)
for {
select {
case <-runCtx.Done():
return nil
case height := <-newBlockChan:
m.Lock()
m.currentHeight = height
m.Unlock()
case err := <-newBlockErrChan:
return err
}
}
}
// recoverInstantOuts recovers all the active instantouts from the database.
func (m *Manager) recoverInstantOuts(ctx context.Context) error {
// Fetch all the active instantouts from the database.
activeInstantOuts, err := m.cfg.Store.ListInstantLoopOuts(ctx)
if err != nil {
return err
}
for _, instantOut := range activeInstantOuts {
if isFinalState(instantOut.State) {
continue
}
log.Debugf("Recovering instantout %v", instantOut.SwapHash)
instantOutFSM, err := NewFSMFromInstantOut(
ctx, m.cfg, instantOut,
)
if err != nil {
return err
}
m.activeInstantOuts[instantOut.SwapHash] = instantOutFSM
// As SendEvent can block, we'll start a goroutine to process
// the event.
go func() {
err := instantOutFSM.SendEvent(OnRecover, nil)
if err != nil {
log.Errorf("FSM %v Error sending recover "+
"event %v, state: %v",
instantOutFSM.InstantOut.SwapHash, err,
instantOutFSM.InstantOut.State)
}
}()
}
return nil
}
// NewInstantOut creates a new instantout.
func (m *Manager) NewInstantOut(ctx context.Context,
reservations []reservation.ID, sweepAddress string) (*FSM, error) {
var (
sweepAddr btcutil.Address
err error
)
if sweepAddress != "" {
sweepAddr, err = btcutil.DecodeAddress(
sweepAddress, m.cfg.Network,
)
if err != nil {
return nil, err
}
}
m.Lock()
// Create the instantout request.
request := &InitInstantOutCtx{
cltvExpiry: m.currentHeight + int32(defaultCltv),
reservations: reservations,
initationHeight: m.currentHeight,
protocolVersion: CurrentProtocolVersion(),
sweepAddress: sweepAddr,
}
instantOut, err := NewFSM(
m.runCtx, m.cfg, ProtocolVersionFullReservation,
)
if err != nil {
m.Unlock()
return nil, err
}
m.activeInstantOuts[instantOut.InstantOut.SwapHash] = instantOut
m.Unlock()
// Start the instantout FSM.
go func() {
err := instantOut.SendEvent(OnStart, request)
if err != nil {
log.Errorf("Error sending event: %v", err)
}
}()
// If everything went well, we'll wait for the instant out to be
// waiting for sweepless sweep to be confirmed.
err = instantOut.DefaultObserver.WaitForState(
ctx, defaultStateWaitTime, WaitForSweeplessSweepConfirmed,
fsm.WithAbortEarlyOnErrorOption(),
)
if err != nil {
return nil, err
}
return instantOut, nil
}
// GetActiveInstantOut returns an active instant out.
func (m *Manager) GetActiveInstantOut(swapHash lntypes.Hash) (*FSM, error) {
m.Lock()
defer m.Unlock()
fsm, ok := m.activeInstantOuts[swapHash]
if !ok {
return nil, ErrSwapDoesNotExist
}
// If the instant out is in a final state, we'll remove it from the
// active instant outs.
if isFinalState(fsm.InstantOut.State) {
delete(m.activeInstantOuts, swapHash)
}
return fsm, nil
}
type Quote struct {
// ServiceFee is the fee in sat that is paid to the loop service.
ServiceFee btcutil.Amount
// OnChainFee is the estimated on chain fee in sat.
OnChainFee btcutil.Amount
}
// GetInstantOutQuote returns a quote for an instant out.
func (m *Manager) GetInstantOutQuote(ctx context.Context,
amt btcutil.Amount, numReservations int) (Quote, error) {
if numReservations <= 0 {
return Quote{}, fmt.Errorf("no reservations selected")
}
if amt <= 0 {
return Quote{}, fmt.Errorf("no amount selected")
}
// Get the service fee.
quoteRes, err := m.cfg.InstantOutClient.GetInstantOutQuote(
ctx, &looprpc.GetInstantOutQuoteRequest{
Amount: uint64(amt),
},
)
if err != nil {
return Quote{}, err
}
// Get the offchain fee by getting the fee estimate from the lnd client
// and multiplying it by the estimated sweepless sweep transaction.
feeRate, err := m.cfg.Wallet.EstimateFeeRate(ctx, normalConfTarget)
if err != nil {
return Quote{}, err
}
// The on chain chainFee is the chainFee rate times the estimated
// sweepless sweep transaction size.
chainFee := feeRate.FeeForWeight(sweeplessSweepWeight(numReservations))
return Quote{
ServiceFee: btcutil.Amount(quoteRes.SwapFee),
OnChainFee: chainFee,
}, nil
}
// ListInstantOuts returns all instant outs from the database.
func (m *Manager) ListInstantOuts(ctx context.Context) ([]*InstantOut, error) {
return m.cfg.Store.ListInstantLoopOuts(ctx)
}

@ -1,239 +0,0 @@
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"
"github.com/lightningnetwork/lnd/chainntnfs"
)
// 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 (f *FSM) InitAction(eventCtx fsm.EventContext) fsm.EventType {
// Check if the context is of the correct type.
reservationRequest, ok := eventCtx.(*InitReservationContext)
if !ok {
return f.HandleError(fsm.ErrInvalidContextType)
}
keyRes, err := f.cfg.Wallet.DeriveNextKey(
f.ctx, KeyFamily,
)
if err != nil {
return f.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 = f.cfg.ReservationClient.OpenReservation(f.ctx, request)
if err != nil {
return f.HandleError(err)
}
reservation, err := NewReservation(
reservationRequest.reservationID,
reservationRequest.serverPubkey,
keyRes.PubKey,
reservationRequest.value,
reservationRequest.expiry,
reservationRequest.heightHint,
keyRes.KeyLocator,
)
if err != nil {
return f.HandleError(err)
}
f.reservation = reservation
// Create the reservation in the database.
err = f.cfg.Store.CreateReservation(f.ctx, reservation)
if err != nil {
return f.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 (f *FSM) SubscribeToConfirmationAction(_ fsm.EventContext) fsm.EventType {
pkscript, err := f.reservation.GetPkScript()
if err != nil {
return f.HandleError(err)
}
callCtx, cancel := context.WithCancel(f.ctx)
defer cancel()
// Subscribe to the confirmation of the reservation transaction.
log.Debugf("Subscribing to conf for reservation: %x pkscript: %x, "+
"initiation height: %v", f.reservation.ID, pkscript,
f.reservation.InitiationHeight)
confChan, errConfChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn(
callCtx, nil, pkscript, DefaultConfTarget,
f.reservation.InitiationHeight,
)
if err != nil {
f.Errorf("unable to subscribe to conf notification: %v", err)
return f.HandleError(err)
}
blockChan, errBlockChan, err := f.cfg.ChainNotifier.RegisterBlockEpochNtfn(
callCtx,
)
if err != nil {
f.Errorf("unable to subscribe to block notifications: %v", err)
return f.HandleError(err)
}
// We'll now wait for the confirmation of the reservation transaction.
for {
select {
case err := <-errConfChan:
f.Errorf("conf subscription error: %v", err)
return f.HandleError(err)
case err := <-errBlockChan:
f.Errorf("block subscription error: %v", err)
return f.HandleError(err)
case confInfo := <-confChan:
f.Debugf("confirmed in tx: %v", confInfo.Tx)
outpoint, err := f.reservation.findReservationOutput(
confInfo.Tx,
)
if err != nil {
return f.HandleError(err)
}
f.reservation.ConfirmationHeight = confInfo.BlockHeight
f.reservation.Outpoint = outpoint
return OnConfirmed
case block := <-blockChan:
f.Debugf("block received: %v expiry: %v", block,
f.reservation.Expiry)
if uint32(block) >= f.reservation.Expiry {
return OnTimedOut
}
case <-f.ctx.Done():
return fsm.NoOp
}
}
}
// AsyncWaitForExpiredOrSweptAction waits for the reservation to be either
// expired or swept. This is non-blocking and can be used to wait for the
// reservation to expire while expecting other events.
func (f *FSM) AsyncWaitForExpiredOrSweptAction(_ fsm.EventContext,
) fsm.EventType {
notifCtx, cancel := context.WithCancel(f.ctx)
blockHeightChan, errEpochChan, err := f.cfg.ChainNotifier.
RegisterBlockEpochNtfn(notifCtx)
if err != nil {
cancel()
return f.HandleError(err)
}
pkScript, err := f.reservation.GetPkScript()
if err != nil {
cancel()
return f.HandleError(err)
}
spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterSpendNtfn(
notifCtx, f.reservation.Outpoint, pkScript,
f.reservation.InitiationHeight,
)
if err != nil {
cancel()
return f.HandleError(err)
}
go func() {
defer cancel()
op, err := f.handleSubcriptions(
notifCtx, blockHeightChan, spendChan, errEpochChan,
errSpendChan,
)
if err != nil {
f.handleAsyncError(err)
return
}
if op == fsm.NoOp {
return
}
err = f.SendEvent(op, nil)
if err != nil {
f.Errorf("Error sending %s event: %v", op, err)
}
}()
return fsm.NoOp
}
func (f *FSM) handleSubcriptions(ctx context.Context,
blockHeightChan <-chan int32, spendChan <-chan *chainntnfs.SpendDetail,
errEpochChan <-chan error, errSpendChan <-chan error,
) (fsm.EventType, error) {
for {
select {
case err := <-errEpochChan:
return fsm.OnError, err
case err := <-errSpendChan:
return fsm.OnError, err
case blockHeight := <-blockHeightChan:
expired := blockHeight >= int32(f.reservation.Expiry)
if expired {
f.Debugf("Reservation expired")
return OnTimedOut, nil
}
case <-spendChan:
return OnSpent, nil
case <-ctx.Done():
return fsm.NoOp, nil
}
}
}
func (f *FSM) handleAsyncError(err error) {
f.LastActionError = err
f.Errorf("Error on async action: %v", err)
err2 := f.SendEvent(fsm.OnError, err)
if err2 != nil {
f.Errorf("Error sending event: %v", err2)
}
}

@ -1,459 +0,0 @@
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 {
tc := tc
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 {
tc := tc
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)
})
}
}
// AsyncWaitForExpiredOrSweptAction tests the AsyncWaitForExpiredOrSweptAction
// of the reservation state machine.
func TestAsyncWaitForExpiredOrSweptAction(t *testing.T) {
tests := []struct {
name string
blockErr error
spendErr error
expectedEvent fsm.EventType
}{
{
name: "noop",
expectedEvent: fsm.NoOp,
},
{
name: "block error",
blockErr: errors.New("block error"),
expectedEvent: fsm.OnError,
},
{
name: "spend error",
spendErr: errors.New("spend error"),
expectedEvent: fsm.OnError,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) { // Create a mock ChainNotifier and Reservation
chainNotifier := new(MockChainNotifier)
// Define your FSM
r := NewFSMFromReservation(
context.Background(), &Config{
ChainNotifier: chainNotifier,
},
&Reservation{
ServerPubkey: defaultPubkey,
ClientPubkey: defaultPubkey,
Expiry: defaultExpiry,
},
)
// Define the expected return values for your mocks
chainNotifier.On("RegisterBlockEpochNtfn", mock.Anything).Return(
make(chan int32), make(chan error), tc.blockErr,
)
chainNotifier.On(
"RegisterSpendNtfn", mock.Anything,
mock.Anything, mock.Anything,
).Return(
make(chan *chainntnfs.SpendDetail),
make(chan error), tc.spendErr,
)
eventType := r.AsyncWaitForExpiredOrSweptAction(nil)
// Assert that the return value is as expected
require.Equal(t, tc.expectedEvent, eventType)
})
}
}
// TesthandleSubcriptions tests the handleSubcriptions function of the
// reservation state machine.
func TestHandleSubcriptions(t *testing.T) {
var (
blockErr = errors.New("block error")
spendErr = errors.New("spend error")
)
tests := []struct {
name string
blockHeight int32
blockErr error
spendDetail *chainntnfs.SpendDetail
spendErr error
expectedEvent fsm.EventType
expectedErr error
}{
{
name: "expired",
blockHeight: 100,
expectedEvent: OnTimedOut,
},
{
name: "block error",
blockErr: blockErr,
expectedEvent: fsm.OnError,
expectedErr: blockErr,
},
{
name: "spent",
spendDetail: &chainntnfs.SpendDetail{},
expectedEvent: OnSpent,
},
{
name: "spend error",
spendErr: spendErr,
expectedEvent: fsm.OnError,
expectedErr: spendErr,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
chainNotifier := new(MockChainNotifier)
// Create the FSM.
r := NewFSMFromReservation(
context.Background(), &Config{
ChainNotifier: chainNotifier,
},
&Reservation{
ServerPubkey: defaultPubkey,
ClientPubkey: defaultPubkey,
Expiry: defaultExpiry,
},
)
blockChan := make(chan int32)
blockErrChan := make(chan error)
spendChan := make(chan *chainntnfs.SpendDetail)
spendErrChan := make(chan error)
go func() {
if tc.blockHeight != 0 {
blockChan <- tc.blockHeight
}
if tc.blockErr != nil {
blockErrChan <- tc.blockErr
}
if tc.spendDetail != nil {
spendChan <- tc.spendDetail
}
if tc.spendErr != nil {
spendErrChan <- tc.spendErr
}
}()
eventType, err := r.handleSubcriptions(
context.Background(), blockChan, spendChan,
blockErrChan, spendErrChan,
)
require.Equal(t, tc.expectedErr, err)
require.Equal(t, tc.expectedEvent, eventType)
})
}
}

@ -1,265 +0,0 @@
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")
// OnSpent is the event that is triggered when the reservation has been
// spent.
OnSpent = fsm.EventType("OnSpent")
// OnLocked is the event that is triggered when the reservation has
// been locked.
OnLocked = fsm.EventType("OnLocked")
// OnUnlocked is the event that is triggered when the reservation has
// been unlocked.
OnUnlocked = fsm.EventType("OnUnlocked")
)
// 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{
OnSpent: Spent,
OnTimedOut: TimedOut,
OnRecover: Confirmed,
OnLocked: Locked,
fsm.OnError: Confirmed,
},
Action: f.AsyncWaitForExpiredOrSweptAction,
},
Locked: fsm.State{
Transitions: fsm.Transitions{
OnUnlocked: Confirmed,
OnTimedOut: TimedOut,
OnRecover: Locked,
OnSpent: Spent,
fsm.OnError: Locked,
},
Action: f.AsyncWaitForExpiredOrSweptAction,
},
TimedOut: fsm.State{
Transitions: fsm.Transitions{
OnTimedOut: TimedOut,
},
Action: fsm.NoOpAction,
},
Spent: fsm.State{
Transitions: fsm.Transitions{
OnSpent: Spent,
},
Action: fsm.NoOpAction,
},
Failed: fsm.State{
Action: fsm.NoOpAction,
},
}
}
// 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
}

@ -1,33 +0,0 @@
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)
}

@ -1,26 +0,0 @@
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
}

@ -1,348 +0,0 @@
package reservation
import (
"context"
"fmt"
"strings"
"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
// hasL402 is true if the client has a valid L402.
hasL402 bool
runCtx context.Context
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 {
log.Debugf("Starting reservation manager")
runCtx, cancel := context.WithCancel(ctx)
defer cancel()
m.runCtx = runCtx
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(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
}
// fetchL402 fetches the L402 from the server. This method will keep on
// retrying until it gets a valid response.
func (m *Manager) fetchL402(ctx context.Context) {
// Add a 0 timer so that we initially fetch the L402 immediately.
timer := time.NewTimer(0)
for {
select {
case <-ctx.Done():
return
case <-timer.C:
err := m.cfg.FetchL402(ctx)
if err != nil {
log.Warnf("Error fetching L402: %v", err)
timer.Reset(time.Second * 10)
continue
}
m.hasL402 = true
return
}
}
}
// RegisterReservationNotifications registers a new reservation notification
// stream.
func (m *Manager) RegisterReservationNotifications(
reservationChan chan *reservationrpc.ServerReservationNotification) error {
// In order to create a valid l402 we first are going to call
// the FetchL402 method. As a client might not have outbound capacity
// yet, we'll retry until we get a valid response.
if !m.hasL402 {
m.fetchL402(m.runCtx)
}
ctx, cancel := context.WithCancel(m.runCtx)
// We'll now subscribe to the reservation notifications.
reservationStream, err := m.cfg.ReservationClient.
ReservationNotificationStream(
ctx, &reservationrpc.ReservationNotificationRequest{},
)
if err != nil {
cancel()
return err
}
log.Debugf("Successfully subscribed to reservation notifications")
// 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)
cancel()
// If we encounter an error, we'll
// try to reconnect.
for {
select {
case <-m.runCtx.Done():
return
case <-time.After(time.Second * 10):
log.Debugf("Reconnecting to " +
"reservation notifications")
err = m.RegisterReservationNotifications(
reservationChan,
)
if err != nil {
log.Errorf("Error "+
"reconnecting: %v", err)
continue
}
// If we were able to reconnect, we'll
// return.
return
}
}
}
}()
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)
}
// GetReservation returns the reservation for the given id.
func (m *Manager) GetReservation(ctx context.Context, id ID) (*Reservation,
error) {
return m.cfg.Store.GetReservation(ctx, id)
}
// LockReservation locks the reservation with the given ID.
func (m *Manager) LockReservation(ctx context.Context, id ID) error {
// Try getting the reservation from the active reservations map.
m.Lock()
reservation, ok := m.activeReservations[id]
m.Unlock()
if !ok {
return fmt.Errorf("reservation not found")
}
// Try to send the lock event to the reservation.
err := reservation.SendEvent(OnLocked, nil)
if err != nil {
return err
}
return nil
}
// UnlockReservation unlocks the reservation with the given ID.
func (m *Manager) UnlockReservation(ctx context.Context, id ID) error {
// Try getting the reservation from the active reservations map.
m.Lock()
reservation, ok := m.activeReservations[id]
m.Unlock()
if !ok {
return fmt.Errorf("reservation not found")
}
// Try to send the unlock event to the reservation.
err := reservation.SendEvent(OnUnlocked, nil)
if err != nil && strings.Contains(err.Error(), "config error") {
// If the error is a config error, we can ignore it, as the
// reservation is already unlocked.
return nil
} else if err != nil {
return err
}
return nil
}

@ -1,176 +0,0 @@
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.
reservationFSM, 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 := reservationFSM.reservation.GetPkScript()
require.NoError(t, err)
confReg := <-testContext.mockLnd.RegisterConfChannel
require.Equal(t, confReg.PkScript, pkScript)
confTx := &wire.MsgTx{
TxOut: []*wire.TxOut{
{
PkScript: pkScript,
},
},
}
// We'll now confirm the spend.
confReg.ConfChan <- &chainntnfs.TxConfirmation{
BlockHeight: uint32(testContext.mockLnd.Height),
Tx: confTx,
}
// We'll now expect the reservation to be confirmed.
err = reservationFSM.DefaultObserver.WaitForState(ctxb, 5*time.Second, Confirmed)
require.NoError(t, err)
// We'll now expect a spend registration.
spendReg := <-testContext.mockLnd.RegisterSpendChannel
require.Equal(t, spendReg.PkScript, pkScript)
go func() {
// We'll expect a second spend registration.
spendReg = <-testContext.mockLnd.RegisterSpendChannel
require.Equal(t, spendReg.PkScript, pkScript)
}()
// We'll now try to lock the reservation.
err = testContext.manager.LockReservation(ctxb, defaultReservationId)
require.NoError(t, err)
// We'll try to lock the reservation again, which should fail.
err = testContext.manager.LockReservation(ctxb, defaultReservationId)
require.Error(t, err)
testContext.mockLnd.SpendChannel <- &chainntnfs.SpendDetail{
SpentOutPoint: spendReg.Outpoint,
}
// We'll now expect the reservation to be expired.
err = reservationFSM.DefaultObserver.WaitForState(ctxb, 5*time.Second, Spent)
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
}

@ -1,179 +0,0 @@
package reservation
import (
"bytes"
"context"
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/fsm"
reservation_script "github.com/lightninglabs/loop/instantout/reservation/script"
"github.com/lightningnetwork/lnd/input"
"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
}
// Output returns the reservation output.
func (r *Reservation) Output() (*wire.TxOut, error) {
pkscript, err := r.GetPkScript()
if err != nil {
return nil, err
}
return wire.NewTxOut(int64(r.Value), 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")
}
// Musig2CreateSession creates a musig2 session for the reservation.
func (r *Reservation) Musig2CreateSession(ctx context.Context,
signer lndclient.SignerClient) (*input.MuSig2SessionInfo, error) {
signers := [][]byte{
r.ClientPubkey.SerializeCompressed(),
r.ServerPubkey.SerializeCompressed(),
}
expiryLeaf, err := reservation_script.TaprootExpiryScript(
r.Expiry, r.ServerPubkey,
)
if err != nil {
return nil, err
}
rootHash := expiryLeaf.TapHash()
musig2SessionInfo, err := signer.MuSig2CreateSession(
ctx, input.MuSig2Version100RC2, &r.KeyLocator, signers,
lndclient.MuSig2TaprootTweakOpt(rootHash[:], false),
)
if err != nil {
return nil, err
}
return musig2SessionInfo, nil
}

@ -1,21 +0,0 @@
```mermaid
stateDiagram-v2
[*] --> Init: OnServerRequest
Confirmed
Confirmed --> SpendBroadcasted: OnSpendBroadcasted
Confirmed --> TimedOut: OnTimedOut
Confirmed --> Confirmed: OnRecover
Failed
Init
Init --> WaitForConfirmation: OnBroadcast
Init --> Failed: OnRecover
Init --> Failed: OnError
SpendBroadcasted
SpendBroadcasted --> SpendConfirmed: OnSpendConfirmed
SpendConfirmed
TimedOut
WaitForConfirmation
WaitForConfirmation --> WaitForConfirmation: OnRecover
WaitForConfirmation --> Confirmed: OnConfirmed
WaitForConfirmation --> TimedOut: OnTimedOut
```

@ -1,122 +0,0 @@
package script
import (
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/txscript"
"github.com/lightningnetwork/lnd/input"
)
const (
// TaprootMultiSigWitnessSize evaluates to 66 bytes:
// - num_witness_elements: 1 byte
// - sig_varint_len: 1 byte
// - <sig>: 64 bytes
TaprootMultiSigWitnessSize = 1 + 1 + 64
// TaprootExpiryScriptSize evaluates to 39 bytes:
// - OP_DATA: 1 byte (trader_key length)
// - <trader_key>: 32 bytes
// - OP_CHECKSIGVERIFY: 1 byte
// - <reservation_expiry>: 4 bytes
// - OP_CHECKLOCKTIMEVERIFY: 1 byte
TaprootExpiryScriptSize = 1 + 32 + 1 + 4 + 1
// TaprootExpiryWitnessSize evaluates to 140 bytes:
// - num_witness_elements: 1 byte
// - trader_sig_varint_len: 1 byte (trader_sig length)
// - <trader_sig>: 64 bytes
// - witness_script_varint_len: 1 byte (script length)
// - <witness_script>: 39 bytes
// - control_block_varint_len: 1 byte (control block length)
// - <control_block>: 33 bytes
TaprootExpiryWitnessSize = 1 + 1 + 64 + 1 + TaprootExpiryScriptSize + 1 + 33
)
// ReservationScript returns the tapscript pkscript for the given reservation
// parameters.
func ReservationScript(expiry uint32, serverKey,
clientKey *btcec.PublicKey) ([]byte, error) {
aggregatedKey, err := TaprootKey(expiry, serverKey, clientKey)
if err != nil {
return nil, err
}
return PayToWitnessTaprootScript(aggregatedKey.FinalKey)
}
// TaprootKey returns the aggregated MuSig2 combined key.
func TaprootKey(expiry uint32, serverKey,
clientKey *btcec.PublicKey) (*musig2.AggregateKey, error) {
expiryLeaf, err := TaprootExpiryScript(expiry, serverKey)
if err != nil {
return nil, err
}
rootHash := expiryLeaf.TapHash()
aggregateKey, err := input.MuSig2CombineKeys(
input.MuSig2Version100RC2,
[]*btcec.PublicKey{
clientKey, serverKey,
}, true,
&input.MuSig2Tweaks{
TaprootTweak: rootHash[:],
},
)
if err != nil {
return nil, fmt.Errorf("error combining keys: %v", err)
}
return aggregateKey, nil
}
// PayToWitnessTaprootScript creates a new script to pay to a version 1
// (taproot) witness program.
func PayToWitnessTaprootScript(taprootKey *btcec.PublicKey) ([]byte, error) {
builder := txscript.NewScriptBuilder()
builder.AddOp(txscript.OP_1)
builder.AddData(schnorr.SerializePubKey(taprootKey))
return builder.Script()
}
// TaprootExpiryScript returns the leaf script of the expiry script path.
//
// <server_key> OP_CHECKSIGVERIFY <reservation_expiry> OP_CHECKLOCKTIMEVERIFY.
func TaprootExpiryScript(expiry uint32,
serverKey *btcec.PublicKey) (*txscript.TapLeaf, error) {
builder := txscript.NewScriptBuilder()
builder.AddData(schnorr.SerializePubKey(serverKey))
builder.AddOp(txscript.OP_CHECKSIGVERIFY)
builder.AddInt64(int64(expiry))
builder.AddOp(txscript.OP_CHECKLOCKTIMEVERIFY)
script, err := builder.Script()
if err != nil {
return nil, err
}
leaf := txscript.NewBaseTapLeaf(script)
return &leaf, nil
}
// ExpirySpendWeight returns the weight of the expiry path spend.
func ExpirySpendWeight() int64 {
var weightEstimator input.TxWeightEstimator
weightEstimator.AddWitnessInput(TaprootExpiryWitnessSize)
weightEstimator.AddP2TROutput()
return int64(weightEstimator.Weight())
}

@ -1,298 +0,0 @@
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
}

@ -1,96 +0,0 @@
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
}

@ -1,432 +0,0 @@
package instantout
import (
"bytes"
"context"
"database/sql"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
// InstantOutBaseDB is the interface that contains all the queries generated
// by sqlc for the instantout table.
type InstantOutBaseDB interface {
// InsertSwap inserts a new base swap.
InsertSwap(ctx context.Context, arg sqlc.InsertSwapParams) error
// InsertHtlcKeys inserts the htlc keys for a swap.
InsertHtlcKeys(ctx context.Context, arg sqlc.InsertHtlcKeysParams) error
// InsertInstantOut inserts a new instant out swap.
InsertInstantOut(ctx context.Context,
arg sqlc.InsertInstantOutParams) error
// InsertInstantOutUpdate inserts a new instant out update.
InsertInstantOutUpdate(ctx context.Context,
arg sqlc.InsertInstantOutUpdateParams) error
// UpdateInstantOut updates an instant out swap.
UpdateInstantOut(ctx context.Context,
arg sqlc.UpdateInstantOutParams) error
// GetInstantOutSwap retrieves an instant out swap.
GetInstantOutSwap(ctx context.Context,
swapHash []byte) (sqlc.GetInstantOutSwapRow, error)
// GetInstantOutSwapUpdates retrieves all instant out swap updates.
GetInstantOutSwapUpdates(ctx context.Context,
swapHash []byte) ([]sqlc.InstantoutUpdate, error)
// GetInstantOutSwaps retrieves all instant out swaps.
GetInstantOutSwaps(ctx context.Context) ([]sqlc.GetInstantOutSwapsRow,
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
}
// ReservationStore is the interface that is required to load the reservations
// based on the stored reservation ids.
type ReservationStore interface {
// GetReservation returns the reservation for the given id.
GetReservation(ctx context.Context, id reservation.ID) (
*reservation.Reservation, error)
}
type SQLStore struct {
baseDb InstantOutBaseDB
reservationStore ReservationStore
clock clock.Clock
network *chaincfg.Params
}
// NewSQLStore creates a new SQLStore.
func NewSQLStore(db InstantOutBaseDB, clock clock.Clock,
reservationStore ReservationStore, network *chaincfg.Params) *SQLStore {
return &SQLStore{
baseDb: db,
clock: clock,
reservationStore: reservationStore,
}
}
// CreateInstantLoopOut adds a new instant loop out to the store.
func (s *SQLStore) CreateInstantLoopOut(ctx context.Context,
instantOut *InstantOut) error {
swapArgs := sqlc.InsertSwapParams{
SwapHash: instantOut.SwapHash[:],
Preimage: instantOut.swapPreimage[:],
InitiationTime: s.clock.Now(),
AmountRequested: int64(instantOut.Value),
CltvExpiry: instantOut.CltvExpiry,
MaxMinerFee: 0,
MaxSwapFee: 0,
InitiationHeight: instantOut.initiationHeight,
ProtocolVersion: int32(instantOut.protocolVersion),
Label: "",
}
htlcKeyArgs := sqlc.InsertHtlcKeysParams{
SwapHash: instantOut.SwapHash[:],
SenderScriptPubkey: instantOut.serverPubkey.
SerializeCompressed(),
ReceiverScriptPubkey: instantOut.clientPubkey.
SerializeCompressed(),
ClientKeyFamily: int32(instantOut.keyLocator.Family),
ClientKeyIndex: int32(instantOut.keyLocator.Index),
}
reservationIdByteSlice := reservationIdsToByteSlice(
instantOut.Reservations,
)
instantOutArgs := sqlc.InsertInstantOutParams{
SwapHash: instantOut.SwapHash[:],
Preimage: instantOut.swapPreimage[:],
SweepAddress: instantOut.sweepAddress.String(),
OutgoingChanSet: instantOut.outgoingChanSet.String(),
HtlcFeeRate: int64(instantOut.htlcFeeRate),
ReservationIds: reservationIdByteSlice,
SwapInvoice: instantOut.swapInvoice,
}
updateArgs := sqlc.InsertInstantOutUpdateParams{
SwapHash: instantOut.SwapHash[:],
UpdateTimestamp: s.clock.Now(),
UpdateState: string(instantOut.State),
}
return s.baseDb.ExecTx(ctx, &loopdb.SqliteTxOptions{},
func(q *sqlc.Queries) error {
err := q.InsertSwap(ctx, swapArgs)
if err != nil {
return err
}
err = q.InsertHtlcKeys(ctx, htlcKeyArgs)
if err != nil {
return err
}
err = q.InsertInstantOut(ctx, instantOutArgs)
if err != nil {
return err
}
return q.InsertInstantOutUpdate(ctx, updateArgs)
})
}
// UpdateInstantLoopOut updates an existing instant loop out in the
// store.
func (s *SQLStore) UpdateInstantLoopOut(ctx context.Context,
instantOut *InstantOut) error {
// Serialize the FinalHtlcTx.
var finalHtlcTx []byte
if instantOut.finalizedHtlcTx != nil {
var buffer bytes.Buffer
err := instantOut.finalizedHtlcTx.Serialize(
&buffer,
)
if err != nil {
return err
}
finalHtlcTx = buffer.Bytes()
}
var finalSweeplessSweepTx []byte
if instantOut.FinalizedSweeplessSweepTx != nil {
var buffer bytes.Buffer
err := instantOut.FinalizedSweeplessSweepTx.Serialize(
&buffer,
)
if err != nil {
return err
}
finalSweeplessSweepTx = buffer.Bytes()
}
var sweepTxHash []byte
if instantOut.SweepTxHash != nil {
sweepTxHash = instantOut.SweepTxHash[:]
}
updateParams := sqlc.UpdateInstantOutParams{
SwapHash: instantOut.SwapHash[:],
FinalizedHtlcTx: finalHtlcTx,
SweepTxHash: sweepTxHash,
FinalizedSweeplessSweepTx: finalSweeplessSweepTx,
SweepConfirmationHeight: serializeNullInt32(
int32(instantOut.sweepConfirmationHeight),
),
}
updateArgs := sqlc.InsertInstantOutUpdateParams{
SwapHash: instantOut.SwapHash[:],
UpdateTimestamp: s.clock.Now(),
UpdateState: string(instantOut.State),
}
return s.baseDb.ExecTx(ctx, &loopdb.SqliteTxOptions{},
func(q *sqlc.Queries) error {
err := q.UpdateInstantOut(ctx, updateParams)
if err != nil {
return err
}
return q.InsertInstantOutUpdate(ctx, updateArgs)
},
)
}
// GetInstantLoopOut returns the instant loop out for the given swap
// hash.
func (s *SQLStore) GetInstantLoopOut(ctx context.Context, swapHash []byte) (
*InstantOut, error) {
row, err := s.baseDb.GetInstantOutSwap(ctx, swapHash)
if err != nil {
return nil, err
}
updates, err := s.baseDb.GetInstantOutSwapUpdates(ctx, swapHash)
if err != nil {
return nil, err
}
return s.sqlInstantOutToInstantOut(ctx, row, updates)
}
// ListInstantLoopOuts returns all instant loop outs that are in the
// store.
func (s *SQLStore) ListInstantLoopOuts(ctx context.Context) ([]*InstantOut,
error) {
rows, err := s.baseDb.GetInstantOutSwaps(ctx)
if err != nil {
return nil, err
}
var instantOuts []*InstantOut
for _, row := range rows {
updates, err := s.baseDb.GetInstantOutSwapUpdates(
ctx, row.SwapHash,
)
if err != nil {
return nil, err
}
instantOut, err := s.sqlInstantOutToInstantOut(
ctx, sqlc.GetInstantOutSwapRow(row), updates,
)
if err != nil {
return nil, err
}
instantOuts = append(instantOuts, instantOut)
}
return instantOuts, nil
}
// sqlInstantOutToInstantOut converts sql rows to an instant out struct.
func (s *SQLStore) sqlInstantOutToInstantOut(ctx context.Context,
row sqlc.GetInstantOutSwapRow, updates []sqlc.InstantoutUpdate) (
*InstantOut, error) {
swapHash, err := lntypes.MakeHash(row.SwapHash)
if err != nil {
return nil, err
}
swapPreImage, err := lntypes.MakePreimage(row.Preimage)
if err != nil {
return nil, err
}
serverKey, err := btcec.ParsePubKey(row.SenderScriptPubkey)
if err != nil {
return nil, err
}
clientKey, err := btcec.ParsePubKey(row.ReceiverScriptPubkey)
if err != nil {
return nil, err
}
var finalizedHtlcTx *wire.MsgTx
if row.FinalizedHtlcTx != nil {
finalizedHtlcTx = &wire.MsgTx{}
err := finalizedHtlcTx.Deserialize(bytes.NewReader(
row.FinalizedHtlcTx,
))
if err != nil {
return nil, err
}
}
var finalizedSweepLessSweepTx *wire.MsgTx
if row.FinalizedSweeplessSweepTx != nil {
finalizedSweepLessSweepTx = &wire.MsgTx{}
err := finalizedSweepLessSweepTx.Deserialize(bytes.NewReader(
row.FinalizedSweeplessSweepTx,
))
if err != nil {
return nil, err
}
}
var sweepTxHash *chainhash.Hash
if row.SweepTxHash != nil {
sweepTxHash, err = chainhash.NewHash(row.SweepTxHash)
if err != nil {
return nil, err
}
}
var outgoingChanSet loopdb.ChannelSet
if row.OutgoingChanSet != "" {
outgoingChanSet, err = loopdb.ConvertOutgoingChanSet(
row.OutgoingChanSet,
)
if err != nil {
return nil, err
}
}
reservationIds, err := byteSliceToReservationIds(row.ReservationIds)
if err != nil {
return nil, err
}
reservations := make([]*reservation.Reservation, 0, len(reservationIds))
for _, id := range reservationIds {
reservation, err := s.reservationStore.GetReservation(
ctx, id,
)
if err != nil {
return nil, err
}
reservations = append(reservations, reservation)
}
sweepAddress, err := btcutil.DecodeAddress(row.SweepAddress, s.network)
if err != nil {
return nil, err
}
instantOut := &InstantOut{
SwapHash: swapHash,
swapPreimage: swapPreImage,
CltvExpiry: row.CltvExpiry,
outgoingChanSet: outgoingChanSet,
Reservations: reservations,
protocolVersion: ProtocolVersion(row.ProtocolVersion),
initiationHeight: row.InitiationHeight,
Value: btcutil.Amount(row.AmountRequested),
keyLocator: keychain.KeyLocator{
Family: keychain.KeyFamily(row.ClientKeyFamily),
Index: uint32(row.ClientKeyIndex),
},
clientPubkey: clientKey,
serverPubkey: serverKey,
swapInvoice: row.SwapInvoice,
htlcFeeRate: chainfee.SatPerKWeight(row.HtlcFeeRate),
sweepAddress: sweepAddress,
finalizedHtlcTx: finalizedHtlcTx,
SweepTxHash: sweepTxHash,
FinalizedSweeplessSweepTx: finalizedSweepLessSweepTx,
sweepConfirmationHeight: uint32(deserializeNullInt32(
row.SweepConfirmationHeight,
)),
}
if len(updates) > 0 {
lastUpdate := updates[len(updates)-1]
instantOut.State = fsm.StateType(lastUpdate.UpdateState)
}
return instantOut, nil
}
// reservationIdsToByteSlice converts a slice of reservation ids to a byte
// slice.
func reservationIdsToByteSlice(reservations []*reservation.Reservation) []byte {
var reservationIds []byte
for _, reservation := range reservations {
reservationIds = append(reservationIds, reservation.ID[:]...)
}
return reservationIds
}
// byteSliceToReservationIds converts a byte slice to a slice of reservation
// ids.
func byteSliceToReservationIds(byteSlice []byte) ([]reservation.ID, error) {
if len(byteSlice)%32 != 0 {
return nil, fmt.Errorf("invalid byte slice length")
}
var reservationIds []reservation.ID
for i := 0; i < len(byteSlice); i += 32 {
var id reservation.ID
copy(id[:], byteSlice[i:i+32])
reservationIds = append(reservationIds, id)
}
return reservationIds, nil
}
// serializeNullInt32 serializes an int32 to a sql.NullInt32.
func serializeNullInt32(i int32) sql.NullInt32 {
return sql.NullInt32{
Int32: i,
Valid: true,
}
}
// deserializeNullInt32 deserializes an int32 from a sql.NullInt32.
func deserializeNullInt32(i sql.NullInt32) int32 {
if i.Valid {
return i.Int32
}
return 0
}

@ -1,36 +0,0 @@
package instantout
import (
"crypto/rand"
"testing"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/stretchr/testify/require"
)
func TestConvertingReservations(t *testing.T) {
var resId1, resId2 reservation.ID
// fill the ids with random values.
if _, err := rand.Read(resId1[:]); err != nil {
t.Fatal(err)
}
if _, err := rand.Read(resId2[:]); err != nil {
t.Fatal(err)
}
reservations := []*reservation.Reservation{
{ID: resId1}, {ID: resId2},
}
byteSlice := reservationIdsToByteSlice(reservations)
require.Len(t, byteSlice, 64)
reservationIds, err := byteSliceToReservationIds(byteSlice)
require.NoError(t, err)
require.Len(t, reservationIds, 2)
require.Equal(t, resId1, reservationIds[0])
require.Equal(t, resId2, reservationIds[1])
}

@ -20,11 +20,6 @@ type OutRequest struct {
// Destination address for the swap.
DestAddr btcutil.Address
// IsExternalAddr indicates whether the provided destination address
// does not belong to the underlying wallet. This helps indicate
// whether the sweep of this swap can be batched or not.
IsExternalAddr bool
// MaxSwapRoutingFee is the maximum off-chain fee in msat that may be
// paid for payment to the server. This limit is applied during path
// finding. Typically this value is taken from the response of the
@ -92,12 +87,6 @@ type OutRequest struct {
// initiated the swap (loop CLI, autolooper, LiT UI and so on) and is
// appended to the user agent string.
Initiator string
// PaymentTimeout specifies the payment timeout for the individual
// off-chain payments. As the swap payment may be retried (depending on
// the configured maximum payment timeout) the total time spent may be
// a multiple of this value.
PaymentTimeout time.Duration
}
// Out contains the full details of a loop out request. This includes things
@ -315,7 +304,7 @@ type LoopInSwapInfo struct { // nolint
// where the loop-in funds may be paid.
HtlcAddressP2WSH btcutil.Address
// HtlcAddressP2TR contains the v3 (pay to taproot) htlc address.
// HtlcAddresP2TR contains the v3 (pay to taproot) htlc address.
HtlcAddressP2TR btcutil.Address
// ServerMessages is the human-readable message received from the loop
@ -405,9 +394,3 @@ type ProbeRequest struct {
// Optional hop hints.
RouteHints [][]zpay32.HopHint
}
// AbandonSwapRequest specifies the swap to abandon. It is identified by its
// swap hash.
type AbandonSwapRequest struct {
SwapHash lntypes.Hash
}

@ -17,8 +17,6 @@ const (
// loopInTimeout is the label used for loop in swaps to sweep an HTLC
// that has timed out.
loopInSweepTimeout = "InSweepTimeout"
loopOutBatchSweepSuccess = "BatchOutSweepSuccess -- %d"
)
// LoopOutSweepSuccess returns the label used for loop out swaps to sweep the
@ -27,11 +25,6 @@ func LoopOutSweepSuccess(swapHash string) string {
return fmt.Sprintf(loopdLabelPattern, loopOutSweepSuccess, swapHash)
}
// LoopOutBatchSweepSuccess returns the label used for loop out sweep batcher.
func LoopOutBatchSweepSuccess(batchID int32) string {
return fmt.Sprintf(loopOutBatchSweepSuccess, batchID)
}
// LoopInHtlcLabel returns the label used for loop in swaps to publish an HTLC.
func LoopInHtlcLabel(swapHash string) string {
return fmt.Sprintf(loopdLabelPattern, loopInHtlc, swapHash)

@ -422,7 +422,6 @@ func TestAutoloopAddress(t *testing.T) {
Amount: amt,
// Define the expected destination address.
DestAddr: addr,
IsExternalAddr: true,
MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat(
quote1.PrepayAmount, prepayFeePPM,
@ -440,7 +439,6 @@ func TestAutoloopAddress(t *testing.T) {
Amount: amt,
// Define the expected destination address.
DestAddr: addr,
IsExternalAddr: true,
MaxSwapRoutingFee: maxRouteFee,
MaxPrepayRoutingFee: ppmToSat(
quote2.PrepayAmount, routeFeePPM,

@ -312,26 +312,9 @@ func (c *autoloopTestCtx) autoloop(step *autoloopStep) {
// Assert that we query the server for a quote for each of our
// recommended swaps. Note that this differs from our set of expected
// swaps because we may get quotes for suggested swaps but then just
// log them. The order in c.quoteRequestIn is not deterministic,
// it depends on the order of map traversal (map peerChannels in
// method Manager.SuggestSwaps). So receive from the channel an item
// and then find a corresponding expected item, using amount as a key.
amt2expected := make(map[btcutil.Amount]quoteInRequestResp)
// log them.
for _, expected := range step.quotesIn {
// Make sure all amounts are unique.
require.NotContains(c.t, amt2expected, expected.request.Amount)
amt2expected[expected.request.Amount] = expected
}
for i := 0; i < len(step.quotesIn); i++ {
request := <-c.quoteRequestIn
// Get the expected item, using amount as a key.
expected, has := amt2expected[request.Amount]
require.True(c.t, has)
delete(amt2expected, request.Amount)
assert.Equal(
c.t, expected.request.Amount, request.Amount,
)

@ -46,7 +46,6 @@ import (
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb"
clientrpc "github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/funding"
@ -56,6 +55,8 @@ import (
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/ticker"
"google.golang.org/protobuf/proto"
clientrpc "github.com/lightninglabs/loop/looprpc"
)
const (
@ -449,13 +450,6 @@ func (m *Manager) autoloop(ctx context.Context) error {
// Create a copy of our range var so that we can reference it.
swap := swap
// Check if the parameter for custom address is defined for loop
// outs.
if m.params.DestAddr != nil {
swap.DestAddr = m.params.DestAddr
swap.IsExternalAddr = true
}
go m.dispatchStickyLoopOut(
ctx, swap, defaultAmountBackoffRetry,
defaultAmountBackoff,

@ -77,6 +77,7 @@ func loopInSweepFee(fee chainfee.SatPerKWeight) btcutil.Amount {
maxSize := htlc.MaxTimeoutWitnessSize()
estimator.AddWitnessInput(maxSize)
weight := int64(estimator.Weight())
return fee.FeeForWeight(estimator.Weight())
return fee.FeeForWeight(weight)
}

@ -138,7 +138,6 @@ func (b *loopOutBuilder) buildSwap(ctx context.Context, pubkey route.Vertex,
// already validated them.
request := loop.OutRequest{
Amount: amount,
IsExternalAddr: false,
OutgoingChanSet: chanSet,
MaxPrepayRoutingFee: prepayMaxFee,
MaxSwapRoutingFee: routeMaxFee,
@ -161,11 +160,9 @@ func (b *loopOutBuilder) buildSwap(ctx context.Context, pubkey route.Vertex,
if len(params.Account) > 0 {
account = params.Account
addrType = params.AccountAddrType
request.IsExternalAddr = true
}
if params.DestAddr != nil {
request.DestAddr = params.DestAddr
request.IsExternalAddr = true
} else {
addr, err := b.cfg.Lnd.WalletKit.NextAddr(
ctx, account, addrType, false,

@ -8,12 +8,13 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/lndclient"
clientrpc "github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/swap"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
clientrpc "github.com/lightninglabs/loop/looprpc"
)
var (

@ -177,7 +177,7 @@ func TestCalculateAmount(t *testing.T) {
}
}
// TestSuggestSwap tests swap suggestions for the threshold rule. It does not
// TestSuggestSwaps tests swap suggestions for the threshold rule. It does not
// many different values because we have separate tests for swap amount
// calculation.
func TestSuggestSwap(t *testing.T) {

@ -10,7 +10,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/aperture/l402"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightningnetwork/lnd/cert"
"github.com/lightningnetwork/lnd/lncfg"
@ -76,10 +76,6 @@ var (
defaultLndMacaroon,
)
// DefaultLndRPCTimeout is the default timeout to use when communicating
// with lnd.
DefaultLndRPCTimeout = time.Minute
// DefaultTLSCertPath is the default full path of the autogenerated TLS
// certificate.
DefaultTLSCertPath = filepath.Join(
@ -122,9 +118,6 @@ type lndConfig struct {
MacaroonPath string `long:"macaroonpath" description:"The full path to the single macaroon to use, either the admin.macaroon or a custom baked one. Cannot be specified at the same time as macaroondir. A custom macaroon must contain ALL permissions required for all subservers to work, otherwise permission errors will occur."`
TLSPath string `long:"tlspath" description:"Path to lnd tls certificate"`
// RPCTimeout is the timeout to use when communicating with lnd.
RPCTimeout time.Duration `long:"rpctimeout" description:"The timeout to use when communicating with lnd"`
}
type loopServerConfig struct {
@ -167,17 +160,15 @@ type Config struct {
MaxLogFileSize int `long:"maxlogfilesize" description:"Maximum logfile size in MB."`
DebugLevel string `long:"debuglevel" description:"Logging level for all subsystems {trace, debug, info, warn, error, critical} -- You may also specify <subsystem>=<level>,<subsystem2>=<level>,... to set the log level for individual subsystems -- Use show to list available subsystems"`
MaxLSATCost uint32 `long:"maxlsatcost" hidden:"true"`
MaxLSATFee uint32 `long:"maxlsatfee" hidden:"true"`
MaxL402Cost uint32 `long:"maxl402cost" description:"Maximum cost in satoshis that loopd is going to pay for an L402 token automatically. Does not include routing fees."`
MaxL402Fee uint32 `long:"maxl402fee" description:"Maximum routing fee in satoshis that we are willing to pay while paying for an L402 token."`
MaxLSATCost uint32 `long:"maxlsatcost" description:"Maximum cost in satoshis that loopd is going to pay for an LSAT token automatically. Does not include routing fees."`
MaxLSATFee uint32 `long:"maxlsatfee" description:"Maximum routing fee in satoshis that we are willing to pay while paying for an LSAT token."`
LoopOutMaxParts uint32 `long:"loopoutmaxparts" description:"The maximum number of payment parts that may be used for a loop out swap."`
TotalPaymentTimeout time.Duration `long:"totalpaymenttimeout" description:"The timeout to use for off-chain payments."`
MaxPaymentRetries int `long:"maxpaymentretries" description:"The maximum number of times an off-chain payment may be retried."`
EnableExperimental bool `long:"experimental" description:"Enable experimental features: reservations"`
EnableExperimental bool `long:"experimental" description:"Enable experimental features: taproot HTLCs and MuSig2 loop out sweeps."`
Lnd *lndConfig `group:"lnd" namespace:"lnd"`
@ -215,8 +206,8 @@ func DefaultConfig() Config {
TLSKeyPath: DefaultTLSKeyPath,
TLSValidity: DefaultAutogenValidity,
MacaroonPath: DefaultMacaroonPath,
MaxL402Cost: l402.DefaultMaxCostSats,
MaxL402Fee: l402.DefaultMaxRoutingFeeSats,
MaxLSATCost: lsat.DefaultMaxCostSats,
MaxLSATFee: lsat.DefaultMaxRoutingFeeSats,
LoopOutMaxParts: defaultLoopOutMaxParts,
TotalPaymentTimeout: defaultTotalPaymentTimeout,
MaxPaymentRetries: defaultMaxPaymentRetries,
@ -224,7 +215,6 @@ func DefaultConfig() Config {
Lnd: &lndConfig{
Host: "localhost:10009",
MacaroonPath: DefaultLndMacaroonPath,
RPCTimeout: DefaultLndRPCTimeout,
},
}
}

@ -16,14 +16,9 @@ import (
proxy "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/instantout"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/loopd/perms"
"github.com/lightninglabs/loop/loopdb"
loop_looprpc "github.com/lightninglabs/loop/looprpc"
loop_swaprpc "github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/macaroons"
"google.golang.org/grpc"
@ -51,7 +46,7 @@ type ListenerCfg struct {
// on the passed TLS configuration.
restListener func(*tls.Config) (net.Listener, error)
// getLnd returns a grpc connection to a lnd instance.
// getLnd returns a grpc connection to an lnd instance.
getLnd func(lndclient.Network, *lndConfig) (*lndclient.GrpcLndServices,
error)
}
@ -118,9 +113,9 @@ func New(config *Config, lisCfg *ListenerCfg) *Daemon {
// Start starts loopd in daemon mode. It will listen for grpc connections,
// execute commands and pass back swap status information.
func (d *Daemon) Start() error {
// There should be no reason to start the daemon twice. Therefore,
// return an error if that's tried. This is mostly to guard against
// Start and StartAsSubserver both being called.
// There should be no reason to start the daemon twice. Therefore return
// an error if that's tried. This is mostly to guard against Start and
// StartAsSubserver both being called.
if atomic.AddInt32(&d.started, 1) != 1 {
return errOnlyStartOnce
}
@ -135,7 +130,7 @@ func (d *Daemon) Start() error {
// With lnd connected, initialize everything else, such as the swap
// server client, the swap client RPC server instance and our main swap
// and error handlers. If this fails, then nothing has been started yet,
// and error handlers. If this fails, then nothing has been started yet
// and we can just return the error.
err = d.initialize(true)
if errors.Is(err, bbolt.ErrTimeout) {
@ -231,7 +226,7 @@ func (d *Daemon) startWebServers() error {
grpc.UnaryInterceptor(unaryInterceptor),
grpc.StreamInterceptor(streamInterceptor),
)
loop_looprpc.RegisterSwapClientServer(d.grpcServer, d)
looprpc.RegisterSwapClientServer(d.grpcServer, d)
// Register our debug server if it is compiled in.
d.registerDebugServer()
@ -291,7 +286,7 @@ func (d *Daemon) startWebServers() error {
restProxyDest, "[::]", "[::1]", 1,
)
}
err = loop_looprpc.RegisterSwapClientHandlerFromEndpoint(
err = looprpc.RegisterSwapClientHandlerFromEndpoint(
ctx, mux, restProxyDest, proxyOpts,
)
if err != nil {
@ -322,7 +317,7 @@ func (d *Daemon) startWebServers() error {
err := d.restServer.Serve(d.restListener)
// ErrServerClosed is always returned when the proxy is
// shut down, so don't log it.
if err != nil && !errors.Is(err, http.ErrServerClosed) {
if err != nil && err != http.ErrServerClosed {
// Notify the main error handler goroutine that
// we exited unexpectedly here. We don't have to
// worry about blocking as the internal error
@ -341,7 +336,7 @@ func (d *Daemon) startWebServers() error {
log.Infof("RPC server listening on %s", d.grpcListener.Addr())
err = d.grpcServer.Serve(d.grpcListener)
if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
if err != nil && err != grpc.ErrServerStopped {
// Notify the main error handler goroutine that
// we exited unexpectedly here. We don't have to
// worry about blocking as the internal error
@ -397,51 +392,13 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
log.Infof("Successfully migrated boltdb")
}
// Now that we know where the database will live, we'll go ahead and
// open up the default implementation of it.
chainParams, err := lndclient.Network(d.cfg.Network).ChainParams()
if err != nil {
return err
}
swapDb, baseDb, err := openDatabase(d.cfg, chainParams)
if err != nil {
return err
}
// Run the costs migration.
err = loop.MigrateLoopOutCosts(d.mainCtx, d.lnd.LndServices, swapDb)
if err != nil {
log.Errorf("Cost migration failed: %v", err)
return err
}
sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams)
// Create an instance of the loop client library.
swapClient, clientCleanup, err := getClient(
d.cfg, swapDb, sweeperDb, &d.lnd.LndServices,
)
swapclient, clientCleanup, err := getClient(d.cfg, &d.lnd.LndServices)
if err != nil {
return err
}
d.clientCleanup = clientCleanup
// Create a reservation server client.
reservationClient := loop_swaprpc.NewReservationServiceClient(
swapClient.Conn,
)
// Create an instantout server client.
instantOutClient := loop_swaprpc.NewInstantSwapServerClient(
swapClient.Conn,
)
// Both the client RPC server and the swap server client should stop
// on main context cancel. So we create it early and pass it down.
d.mainCtx, d.mainCtxCancel = context.WithCancel(context.Background())
// Add our debug permissions to our main set of required permissions
// if compiled in.
for endpoint, perm := range debugRequiredPermissions {
@ -495,60 +452,17 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
}
}
var (
reservationManager *reservation.Manager
instantOutManager *instantout.Manager
)
// Create the reservation and instantout managers.
if d.cfg.EnableExperimental {
reservationStore := reservation.NewSQLStore(baseDb)
reservationConfig := &reservation.Config{
Store: reservationStore,
Wallet: d.lnd.WalletKit,
ChainNotifier: d.lnd.ChainNotifier,
ReservationClient: reservationClient,
FetchL402: swapClient.Server.FetchL402,
}
reservationManager = reservation.NewManager(
reservationConfig,
)
// Create the instantout services.
instantOutStore := instantout.NewSQLStore(
baseDb, clock.NewDefaultClock(), reservationStore,
d.lnd.ChainParams,
)
instantOutConfig := &instantout.Config{
Store: instantOutStore,
LndClient: d.lnd.Client,
RouterClient: d.lnd.Router,
ChainNotifier: d.lnd.ChainNotifier,
Signer: d.lnd.Signer,
Wallet: d.lnd.WalletKit,
ReservationManager: reservationManager,
InstantOutClient: instantOutClient,
Network: d.lnd.ChainParams,
}
instantOutManager = instantout.NewInstantOutManager(
instantOutConfig,
)
}
// Now finally fully initialize the swap client RPC server instance.
d.swapClientServer = swapClientServer{
config: d.cfg,
network: lndclient.Network(d.cfg.Network),
impl: swapClient,
liquidityMgr: getLiquidityManager(swapClient),
lnd: &d.lnd.LndServices,
swaps: make(map[lntypes.Hash]loop.SwapInfo),
subscribers: make(map[int]chan<- interface{}),
statusChan: make(chan loop.SwapInfo),
mainCtx: d.mainCtx,
reservationManager: reservationManager,
instantOutManager: instantOutManager,
config: d.cfg,
network: lndclient.Network(d.cfg.Network),
impl: swapclient,
liquidityMgr: getLiquidityManager(swapclient),
lnd: &d.lnd.LndServices,
swaps: make(map[lntypes.Hash]loop.SwapInfo),
subscribers: make(map[int]chan<- interface{}),
statusChan: make(chan loop.SwapInfo),
mainCtx: d.mainCtx,
}
// Retrieve all currently existing swaps from the database.
@ -608,76 +522,13 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
log.Info("Starting liquidity manager")
err := d.liquidityMgr.Run(d.mainCtx)
if err != nil && !errors.Is(err, context.Canceled) {
if err != nil && err != context.Canceled {
d.internalErrChan <- err
}
log.Info("Liquidity manager stopped")
}()
// Start the reservation manager.
if d.reservationManager != nil {
d.wg.Add(1)
go func() {
defer d.wg.Done()
// We need to know the current block height to properly
// initialize the reservation manager.
getInfo, err := d.lnd.Client.GetInfo(d.mainCtx)
if err != nil {
d.internalErrChan <- err
return
}
log.Info("Starting reservation manager")
defer log.Info("Reservation manager stopped")
err = d.reservationManager.Run(
d.mainCtx, int32(getInfo.BlockHeight),
)
if err != nil && !errors.Is(err, context.Canceled) {
d.internalErrChan <- err
}
}()
}
// Start the instant out manager.
if d.instantOutManager != nil {
d.wg.Add(1)
initChan := make(chan struct{})
go func() {
defer d.wg.Done()
getInfo, err := d.lnd.Client.GetInfo(d.mainCtx)
if err != nil {
d.internalErrChan <- err
return
}
log.Info("Starting instantout manager")
defer log.Info("Instantout manager stopped")
err = d.instantOutManager.Run(
d.mainCtx, initChan, int32(getInfo.BlockHeight),
)
if err != nil && !errors.Is(err, context.Canceled) {
d.internalErrChan <- err
}
}()
// Wait for the instantout server to be ready before starting the
// grpc server.
timeOutCtx, cancel := context.WithTimeout(d.mainCtx, 10*time.Second)
select {
case <-timeOutCtx.Done():
cancel()
return fmt.Errorf("reservation server not ready: %v",
timeOutCtx.Err())
case <-initChan:
cancel()
}
}
// Last, start our internal error handler. This will return exactly one
// error or nil on the main error channel to inform the caller that
// something went wrong or that shutdown is complete. We don't add to
@ -688,9 +539,9 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
var runtimeErr error
// There are only two ways this goroutine can exit. Either there
// is an internal error or the caller requests a shutdown.
// In both cases we wait for the stop to complete before we
// signal the caller that we're done.
// is an internal error or the caller requests shutdown. In both
// cases we wait for the stop to complete before we signal the
// caller that we're done.
select {
case runtimeErr = <-d.internalErrChan:
log.Errorf("Runtime error in daemon, shutting down: "+
@ -699,7 +550,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
case <-d.quit:
}
// We need to shut down before sending the error on the channel,
// We need to shutdown before sending the error on the channel,
// otherwise a caller might exit the process too early.
d.stop()
cleanupMacaroonStore()
@ -730,7 +581,7 @@ func (d *Daemon) stop() {
d.mainCtxCancel()
}
// As there is no swap activity anymore, we can forcefully shut down the
// As there is no swap activity anymore, we can forcefully shutdown the
// gRPC and HTTP servers now.
log.Infof("Stopping gRPC server")
if d.grpcServer != nil {

@ -2,15 +2,12 @@ package loopd
import (
"github.com/btcsuite/btclog"
"github.com/lightninglabs/aperture/l402"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd"
"github.com/lightningnetwork/lnd/build"
"github.com/lightningnetwork/lnd/signal"
@ -34,20 +31,13 @@ func SetupLoggers(root *build.RotatingLogWriter, intercept signal.Interceptor) {
lnd.SetSubLogger(root, Subsystem, log)
lnd.AddSubLogger(root, "LOOP", intercept, loop.UseLogger)
lnd.AddSubLogger(root, "SWEEP", intercept, sweepbatcher.UseLogger)
lnd.AddSubLogger(root, "LNDC", intercept, lndclient.UseLogger)
lnd.AddSubLogger(root, "STORE", intercept, loopdb.UseLogger)
lnd.AddSubLogger(root, l402.Subsystem, intercept, l402.UseLogger)
lnd.AddSubLogger(root, lsat.Subsystem, intercept, lsat.UseLogger)
lnd.AddSubLogger(
root, liquidity.Subsystem, intercept, liquidity.UseLogger,
)
lnd.AddSubLogger(root, fsm.Subsystem, intercept, fsm.UseLogger)
lnd.AddSubLogger(
root, reservation.Subsystem, intercept, reservation.UseLogger,
)
lnd.AddSubLogger(
root, instantout.Subsystem, intercept, instantout.UseLogger,
)
}
// genSubLogger creates a logger for a subsystem. We provide an instance of

@ -2,6 +2,7 @@ package loopd
import (
"context"
"fmt"
"os"
"path/filepath"
@ -25,14 +26,34 @@ func migrateBoltdb(ctx context.Context, cfg *Config) error {
}
defer boltdb.Close()
swapDb, _, err := openDatabase(cfg, chainParams)
var db loopdb.SwapStore
switch cfg.DatabaseBackend {
case DatabaseBackendSqlite:
log.Infof("Opening sqlite3 database at: %v",
cfg.Sqlite.DatabaseFileName)
db, err = loopdb.NewSqliteStore(
cfg.Sqlite, chainParams,
)
case DatabaseBackendPostgres:
log.Infof("Opening postgres database at: %v",
cfg.Postgres.DSN(true))
db, err = loopdb.NewPostgresStore(
cfg.Postgres, chainParams,
)
default:
return fmt.Errorf("unknown database backend: %s",
cfg.DatabaseBackend)
}
if err != nil {
return err
return fmt.Errorf("unable to open database: %v", err)
}
defer swapDb.Close()
defer db.Close()
// Create a new migrator manager.
migrator := loopdb.NewMigratorManager(boltdb, swapDb)
migrator := loopdb.NewMigratorManager(boltdb, db)
// Run the migration.
err = migrator.RunMigrations(ctx)

@ -31,16 +31,6 @@ var RequiredPermissions = map[string][]bakery.Op{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/AbandonSwap": {{
Entity: "swap",
Action: "execute",
}, {
Entity: "loop",
Action: "in",
}, {
Entity: "loop",
Action: "out",
}},
"/looprpc.SwapClient/LoopOutTerms": {{
Entity: "terms",
Action: "read",
@ -69,10 +59,6 @@ var RequiredPermissions = map[string][]bakery.Op{
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/GetL402Tokens": {{
Entity: "auth",
Action: "read",
}},
"/looprpc.SwapClient/GetLsatTokens": {{
Entity: "auth",
Action: "read",
@ -100,20 +86,4 @@ var RequiredPermissions = map[string][]bakery.Op{
Entity: "loop",
Action: "in",
}},
"/looprpc.SwapClient/ListReservations": {{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/InstantOut": {{
Entity: "swap",
Action: "execute",
}},
"/looprpc.SwapClient/InstantOutQuote": {{
Entity: "swap",
Action: "read",
}},
"/looprpc.SwapClient/ListInstantOuts": {{
Entity: "swap",
Action: "read",
}},
}

@ -26,7 +26,7 @@ var (
// listed build tags/subservers need to be enabled.
LoopMinRequiredLndVersion = &verrpc.Version{
AppMajor: 0,
AppMinor: 17,
AppMinor: 16,
AppPatch: 0,
BuildTags: []string{
"signrpc", "walletrpc", "chainrpc", "invoicesrpc",
@ -96,7 +96,6 @@ func NewListenerConfig(config *Config, rpcCfg RPCConfig) *ListenerCfg {
BlockUntilChainSynced: true,
CallerCtx: callerCtx,
BlockUntilUnlocked: true,
RPCTimeout: cfg.RPCTimeout,
}
// If a custom lnd connection is specified we use that

@ -6,20 +6,16 @@ import (
"encoding/hex"
"errors"
"fmt"
"reflect"
"sort"
"strings"
"sync"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/aperture/l402"
"github.com/lightninglabs/aperture/lsat"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/instantout"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb"
@ -76,19 +72,17 @@ type swapClientServer struct {
clientrpc.UnimplementedSwapClientServer
clientrpc.UnimplementedDebugServer
config *Config
network lndclient.Network
impl *loop.Client
liquidityMgr *liquidity.Manager
lnd *lndclient.LndServices
reservationManager *reservation.Manager
instantOutManager *instantout.Manager
swaps map[lntypes.Hash]loop.SwapInfo
subscribers map[int]chan<- interface{}
statusChan chan loop.SwapInfo
nextSubscriberID int
swapsLock sync.Mutex
mainCtx context.Context
config *Config
network lndclient.Network
impl *loop.Client
liquidityMgr *liquidity.Manager
lnd *lndclient.LndServices
swaps map[lntypes.Hash]loop.SwapInfo
subscribers map[int]chan<- interface{}
statusChan chan loop.SwapInfo
nextSubscriberID int
swapsLock sync.Mutex
mainCtx context.Context
}
// LoopOut initiates a loop out swap with the given parameters. The call returns
@ -101,19 +95,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
log.Infof("Loop out request received")
// Note that LoopOutRequest.PaymentTimeout is unsigned and therefore
// cannot be negative.
paymentTimeout := time.Duration(in.PaymentTimeout) * time.Second
// Make sure we don't exceed the total allowed payment timeout.
if paymentTimeout > s.config.TotalPaymentTimeout {
return nil, fmt.Errorf("payment timeout %v exceeds maximum "+
"allowed timeout of %v", paymentTimeout,
s.config.TotalPaymentTimeout)
}
var sweepAddr btcutil.Address
var isExternalAddr bool
var err error
//nolint:lll
switch {
@ -131,8 +113,6 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
return nil, fmt.Errorf("decode address: %v", err)
}
isExternalAddr = true
case in.Account != "" && in.AccountAddrType == clientrpc.AddressType_ADDRESS_TYPE_UNKNOWN:
return nil, liquidity.ErrAccountAndAddrType
@ -157,8 +137,6 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
"%v", err)
}
isExternalAddr = true
default:
// Generate sweep address if none specified.
sweepAddr, err = s.lnd.WalletKit.NextAddr(
@ -184,7 +162,6 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
req := &loop.OutRequest{
Amount: btcutil.Amount(in.Amt),
DestAddr: sweepAddr,
IsExternalAddr: isExternalAddr,
MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
MaxPrepayAmount: btcutil.Amount(in.MaxPrepayAmt),
MaxPrepayRoutingFee: btcutil.Amount(in.MaxPrepayRoutingFee),
@ -195,7 +172,6 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
SwapPublicationDeadline: publicationDeadline,
Label: in.Label,
Initiator: in.Initiator,
PaymentTimeout: paymentTimeout,
}
switch {
@ -312,15 +288,6 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
case loopdb.StateFailIncorrectHtlcAmt:
failureReason = clientrpc.FailureReason_FAILURE_REASON_INCORRECT_AMOUNT
case loopdb.StateFailAbandoned:
failureReason = clientrpc.FailureReason_FAILURE_REASON_ABANDONED
case loopdb.StateFailInsufficientConfirmedBalance:
failureReason = clientrpc.FailureReason_FAILURE_REASON_INSUFFICIENT_CONFIRMED_BALANCE
case loopdb.StateFailIncorrectHtlcAmtSwept:
failureReason = clientrpc.FailureReason_FAILURE_REASON_INCORRECT_HTLC_AMT_SWEPT
default:
return nil, fmt.Errorf("unknown swap state: %v", loopSwap.State)
}
@ -497,11 +464,12 @@ func (s *swapClientServer) Monitor(in *clientrpc.MonitorRequest,
// ListSwaps returns a list of all currently known swaps and their current
// status.
func (s *swapClientServer) ListSwaps(_ context.Context,
req *clientrpc.ListSwapsRequest) (*clientrpc.ListSwapsResponse, error) {
_ *clientrpc.ListSwapsRequest) (*clientrpc.ListSwapsResponse, error) {
var (
rpcSwaps = []*clientrpc.SwapStatus{}
rpcSwaps = make([]*clientrpc.SwapStatus, len(s.swaps))
idx = 0
err error
)
s.swapsLock.Lock()
@ -513,93 +481,15 @@ func (s *swapClientServer) ListSwaps(_ context.Context,
// additional index.
for _, swp := range s.swaps {
swp := swp
// Filter the swap based on the provided filter.
if !filterSwap(&swp, req.ListSwapFilter) {
continue
}
rpcSwap, err := s.marshallSwap(&swp)
rpcSwaps[idx], err = s.marshallSwap(&swp)
if err != nil {
return nil, err
}
rpcSwaps = append(rpcSwaps, rpcSwap)
idx++
}
return &clientrpc.ListSwapsResponse{Swaps: rpcSwaps}, nil
}
// filterSwap filters the given swap based on the provided filter.
func filterSwap(swapInfo *loop.SwapInfo, filter *clientrpc.ListSwapsFilter) bool {
if filter == nil {
return true
}
// If the swap type filter is set, we only return swaps that match the
// filter.
if filter.SwapType != clientrpc.ListSwapsFilter_ANY {
switch filter.SwapType {
case clientrpc.ListSwapsFilter_LOOP_IN:
if swapInfo.SwapType != swap.TypeIn {
return false
}
case clientrpc.ListSwapsFilter_LOOP_OUT:
if swapInfo.SwapType != swap.TypeOut {
return false
}
}
}
// If the pending only filter is set, we only return pending swaps.
if filter.PendingOnly && !swapInfo.State.IsPending() {
return false
}
// If the swap is of type loop out and the outgoing channel filter is
// set, we only return swaps that match the filter.
if swapInfo.SwapType == swap.TypeOut && filter.OutgoingChanSet != nil {
// First we sort both channel sets to make sure we can compare
// them.
sort.Slice(swapInfo.OutgoingChanSet, func(i, j int) bool {
return swapInfo.OutgoingChanSet[i] <
swapInfo.OutgoingChanSet[j]
})
sort.Slice(filter.OutgoingChanSet, func(i, j int) bool {
return filter.OutgoingChanSet[i] <
filter.OutgoingChanSet[j]
})
// Compare the outgoing channel set by using reflect.DeepEqual
// which compares the underlying arrays.
if !reflect.DeepEqual(swapInfo.OutgoingChanSet,
filter.OutgoingChanSet) {
return false
}
}
// If the swap is of type loop in and the last hop filter is set, we
// only return swaps that match the filter.
if swapInfo.SwapType == swap.TypeIn && filter.LoopInLastHop != nil {
// Compare the last hop by using reflect.DeepEqual which
// compares the underlying arrays.
if !reflect.DeepEqual(swapInfo.LastHop, filter.LoopInLastHop) {
return false
}
}
// If a label filter is set, we only return swaps that softly match the
// filter.
if filter.Label != "" {
if !strings.Contains(swapInfo.Label, filter.Label) {
return false
}
}
return true
}
// SwapInfo returns all known details about a single swap.
func (s *swapClientServer) SwapInfo(_ context.Context,
req *clientrpc.SwapInfoRequest) (*clientrpc.SwapStatus, error) {
@ -618,52 +508,9 @@ func (s *swapClientServer) SwapInfo(_ context.Context,
return s.marshallSwap(&swp)
}
// AbandonSwap requests the server to abandon a swap with the given hash.
func (s *swapClientServer) AbandonSwap(ctx context.Context,
req *clientrpc.AbandonSwapRequest) (*clientrpc.AbandonSwapResponse,
error) {
if !req.IKnowWhatIAmDoing {
return nil, fmt.Errorf("please read the AbandonSwap API " +
"documentation")
}
swapHash, err := lntypes.MakeHash(req.Id)
if err != nil {
return nil, fmt.Errorf("error parsing swap hash: %v", err)
}
s.swapsLock.Lock()
swap, ok := s.swaps[swapHash]
s.swapsLock.Unlock()
if !ok {
return nil, fmt.Errorf("swap with hash %s not found", req.Id)
}
if swap.SwapType.IsOut() {
return nil, fmt.Errorf("abandoning loop out swaps is not " +
"supported yet")
}
// If the swap is in a final state, we cannot abandon it.
if swap.State.IsFinal() {
return nil, fmt.Errorf("cannot abandon swap in final state, "+
"state = %s, hash = %s", swap.State.String(), swapHash)
}
err = s.impl.AbandonSwap(ctx, &loop.AbandonSwapRequest{
SwapHash: swapHash,
})
if err != nil {
return nil, fmt.Errorf("error abandoning swap: %v", err)
}
return &clientrpc.AbandonSwapResponse{}, nil
}
// LoopOutTerms returns the terms that the server enforces for loop out swaps.
func (s *swapClientServer) LoopOutTerms(ctx context.Context,
_ *clientrpc.TermsRequest) (*clientrpc.OutTermsResponse, error) {
req *clientrpc.TermsRequest) (*clientrpc.OutTermsResponse, error) {
log.Infof("Loop out terms request received")
@ -693,9 +540,7 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
return nil, err
}
publicactionDeadline := getPublicationDeadline(
req.SwapPublicationDeadline,
)
publicactionDeadline := getPublicationDeadline(req.SwapPublicationDeadline)
quote, err := s.impl.LoopOutQuote(ctx, &loop.LoopOutQuoteRequest{
Amount: btcutil.Amount(req.Amt),
@ -718,7 +563,7 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
// GetLoopInTerms returns the terms that the server enforces for swaps.
func (s *swapClientServer) GetLoopInTerms(ctx context.Context,
_ *clientrpc.TermsRequest) (*clientrpc.InTermsResponse, error) {
req *clientrpc.TermsRequest) (*clientrpc.InTermsResponse, error) {
log.Infof("Loop in terms request received")
@ -932,18 +777,18 @@ func (s *swapClientServer) LoopIn(ctx context.Context,
return response, nil
}
// GetL402Tokens returns all tokens that are contained in the L402 token store.
func (s *swapClientServer) GetL402Tokens(ctx context.Context,
// GetLsatTokens returns all tokens that are contained in the LSAT token store.
func (s *swapClientServer) GetLsatTokens(ctx context.Context,
_ *clientrpc.TokensRequest) (*clientrpc.TokensResponse, error) {
log.Infof("Get L402 tokens request received")
log.Infof("Get LSAT tokens request received")
tokens, err := s.impl.L402Store.AllTokens()
tokens, err := s.impl.LsatStore.AllTokens()
if err != nil {
return nil, err
}
rpcTokens := make([]*clientrpc.L402Token, len(tokens))
rpcTokens := make([]*clientrpc.LsatToken, len(tokens))
idx := 0
for key, token := range tokens {
macBytes, err := token.BaseMacaroon().MarshalBinary()
@ -951,13 +796,13 @@ func (s *swapClientServer) GetL402Tokens(ctx context.Context,
return nil, err
}
id, err := l402.DecodeIdentifier(
id, err := lsat.DecodeIdentifier(
bytes.NewReader(token.BaseMacaroon().Id()),
)
if err != nil {
return nil, err
}
rpcTokens[idx] = &clientrpc.L402Token{
rpcTokens[idx] = &clientrpc.LsatToken{
BaseMacaroon: macBytes,
PaymentHash: token.PaymentHash[:],
PaymentPreimage: token.Preimage[:],
@ -976,21 +821,6 @@ func (s *swapClientServer) GetL402Tokens(ctx context.Context,
return &clientrpc.TokensResponse{Tokens: rpcTokens}, nil
}
// GetLsatTokens returns all tokens that are contained in the L402 token store.
// Deprecated: use GetL402Tokens.
// This API is provided to maintain backward compatibility with gRPC clients
// (e.g. `loop listauth`, Terminal Web, RTL).
// Type LsatToken used by GetLsatTokens in the past was renamed to L402Token,
// but this does not affect binary encoding, so we can use type L402Token here.
func (s *swapClientServer) GetLsatTokens(ctx context.Context,
req *clientrpc.TokensRequest) (*clientrpc.TokensResponse, error) {
log.Warnf("Received deprecated call GetLsatTokens. Please update the " +
"client software. Calling GetL402Tokens now.")
return s.GetL402Tokens(ctx, req)
}
// GetInfo returns basic information about the loop daemon and details to swaps
// from the swap store.
func (s *swapClientServer) GetInfo(ctx context.Context,
@ -1178,127 +1008,6 @@ func (s *swapClientServer) SuggestSwaps(ctx context.Context,
return resp, nil
}
// ListReservations lists all existing reservations the client has ever made.
func (s *swapClientServer) ListReservations(ctx context.Context,
_ *clientrpc.ListReservationsRequest) (
*clientrpc.ListReservationsResponse, error) {
if s.reservationManager == nil {
return nil, status.Error(codes.Unimplemented,
"Restart loop with --experimental")
}
reservations, err := s.reservationManager.GetReservations(
ctx,
)
if err != nil {
return nil, err
}
return &clientrpc.ListReservationsResponse{
Reservations: ToClientReservations(
reservations,
),
}, nil
}
// InstantOut initiates an instant out swap.
func (s *swapClientServer) InstantOut(ctx context.Context,
req *clientrpc.InstantOutRequest) (*clientrpc.InstantOutResponse,
error) {
reservationIds := make([]reservation.ID, len(req.ReservationIds))
for i, id := range req.ReservationIds {
if len(id) != reservation.IdLength {
return nil, fmt.Errorf("invalid reservation id: "+
"expected %v bytes, got %d",
reservation.IdLength, len(id))
}
var resId reservation.ID
copy(resId[:], id)
reservationIds[i] = resId
}
instantOutFsm, err := s.instantOutManager.NewInstantOut(
ctx, reservationIds, req.DestAddr,
)
if err != nil {
return nil, err
}
res := &clientrpc.InstantOutResponse{
InstantOutHash: instantOutFsm.InstantOut.SwapHash[:],
State: string(instantOutFsm.InstantOut.State),
}
if instantOutFsm.InstantOut.SweepTxHash != nil {
res.SweepTxId = instantOutFsm.InstantOut.SweepTxHash.String()
}
return res, nil
}
// InstantOutQuote returns a quote for an instant out swap with the provided
// parameters.
func (s *swapClientServer) InstantOutQuote(ctx context.Context,
req *clientrpc.InstantOutQuoteRequest) (
*clientrpc.InstantOutQuoteResponse, error) {
quote, err := s.instantOutManager.GetInstantOutQuote(
ctx, btcutil.Amount(req.Amt), int(req.NumReservations),
)
if err != nil {
return nil, err
}
return &clientrpc.InstantOutQuoteResponse{
ServiceFeeSat: int64(quote.ServiceFee),
SweepFeeSat: int64(quote.OnChainFee),
}, nil
}
// ListInstantOuts returns a list of all currently known instant out swaps and
// their current status.
func (s *swapClientServer) ListInstantOuts(ctx context.Context,
_ *clientrpc.ListInstantOutsRequest) (
*clientrpc.ListInstantOutsResponse, error) {
instantOuts, err := s.instantOutManager.ListInstantOuts(ctx)
if err != nil {
return nil, err
}
rpcSwaps := make([]*clientrpc.InstantOut, 0, len(instantOuts))
for _, instantOut := range instantOuts {
rpcSwaps = append(rpcSwaps, rpcInstantOut(instantOut))
}
return &clientrpc.ListInstantOutsResponse{
Swaps: rpcSwaps,
}, nil
}
func rpcInstantOut(instantOut *instantout.InstantOut) *clientrpc.InstantOut {
var sweepTxId string
if instantOut.SweepTxHash != nil {
sweepTxId = instantOut.SweepTxHash.String()
}
reservations := make([][]byte, len(instantOut.Reservations))
for i, res := range instantOut.Reservations {
reservations[i] = res.ID[:]
}
return &clientrpc.InstantOut{
SwapHash: instantOut.SwapHash[:],
State: string(instantOut.State),
Amount: uint64(instantOut.Value),
SweepTxId: sweepTxId,
ReservationIds: reservations,
}
}
func rpcAutoloopReason(reason liquidity.Reason) (clientrpc.AutoReason, error) {
switch reason {
case liquidity.ReasonNone:
@ -1558,40 +1267,3 @@ func getPublicationDeadline(unixTimestamp uint64) time.Time {
return time.Unix(int64(unixTimestamp), 0)
}
}
// ToClientReservations converts a slice of server
// reservations to a slice of client reservations.
func ToClientReservations(
res []*reservation.Reservation) []*clientrpc.ClientReservation {
var result []*clientrpc.ClientReservation
for _, r := range res {
result = append(result, toClientReservation(r))
}
return result
}
// toClientReservation converts a server reservation to a
// client reservation.
func toClientReservation(
res *reservation.Reservation) *clientrpc.ClientReservation {
var (
txid string
vout uint32
)
if res.Outpoint != nil {
txid = res.Outpoint.Hash.String()
vout = res.Outpoint.Index
}
return &clientrpc.ClientReservation{
ReservationId: res.ID[:],
State: string(res.State),
Amount: uint64(res.Value),
TxId: txid,
Vout: vout,
Expiry: res.Expiry,
}
}

@ -5,39 +5,18 @@ import (
"fmt"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightninglabs/aperture/l402"
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/liquidity"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/ticker"
)
// getClient returns an instance of the swap client.
func getClient(cfg *Config, swapDb loopdb.SwapStore,
sweeperDb sweepbatcher.BatcherStore, lnd *lndclient.LndServices) (
*loop.Client, func(), error) {
// Default is not set for MaxLSATCost and MaxLSATFee to distinguish
// it from user explicitly setting the option to default value.
// So if MaxL402Cost and MaxLSATFee are not set in the config file
// and command line, they are set to 0.
const (
defaultCost = l402.DefaultMaxCostSats
defaultFee = l402.DefaultMaxRoutingFeeSats
)
if cfg.MaxL402Cost != defaultCost && cfg.MaxLSATCost != 0 {
return nil, nil, fmt.Errorf("both maxl402cost and maxlsatcost" +
" were specified; they are not allowed together")
}
if cfg.MaxL402Fee != defaultFee && cfg.MaxLSATFee != 0 {
return nil, nil, fmt.Errorf("both maxl402fee and maxlsatfee" +
" were specified; they are not allowed together")
}
func getClient(cfg *Config, lnd *lndclient.LndServices) (*loop.Client,
func(), error) {
clientConfig := &loop.ClientConfig{
ServerAddress: cfg.Server.Host,
@ -45,69 +24,50 @@ func getClient(cfg *Config, swapDb loopdb.SwapStore,
SwapServerNoTLS: cfg.Server.NoTLS,
TLSPathServer: cfg.Server.TLSPath,
Lnd: lnd,
MaxL402Cost: btcutil.Amount(cfg.MaxL402Cost),
MaxL402Fee: btcutil.Amount(cfg.MaxL402Fee),
MaxLsatCost: btcutil.Amount(cfg.MaxLSATCost),
MaxLsatFee: btcutil.Amount(cfg.MaxLSATFee),
LoopOutMaxParts: cfg.LoopOutMaxParts,
TotalPaymentTimeout: cfg.TotalPaymentTimeout,
MaxPaymentRetries: cfg.MaxPaymentRetries,
}
if cfg.MaxL402Cost == defaultCost && cfg.MaxLSATCost != 0 {
log.Warnf("Option maxlsatcost is deprecated and will be " +
"removed. Switch to maxl402cost.")
clientConfig.MaxL402Cost = btcutil.Amount(cfg.MaxLSATCost)
}
if cfg.MaxL402Fee == defaultFee && cfg.MaxLSATFee != 0 {
log.Warnf("Option maxlsatfee is deprecated and will be " +
"removed. Switch to maxl402fee.")
clientConfig.MaxL402Fee = btcutil.Amount(cfg.MaxLSATFee)
}
swapClient, cleanUp, err := loop.NewClient(
cfg.DataDir, swapDb, sweeperDb, clientConfig,
)
if err != nil {
return nil, nil, err
}
return swapClient, cleanUp, nil
}
func openDatabase(cfg *Config, chainParams *chaincfg.Params) (loopdb.SwapStore,
*loopdb.BaseDB, error) { //nolint:unparam
// Now that we know where the database will live, we'll go ahead and
// open up the default implementation of it.
var (
db loopdb.SwapStore
err error
baseDb loopdb.BaseDB
db loopdb.SwapStore
err error
)
switch cfg.DatabaseBackend {
case DatabaseBackendSqlite:
log.Infof("Opening sqlite3 database at: %v",
cfg.Sqlite.DatabaseFileName)
db, err = loopdb.NewSqliteStore(cfg.Sqlite, chainParams)
if err != nil {
return nil, nil, err
}
baseDb = *db.(*loopdb.SqliteSwapStore).BaseDB
db, err = loopdb.NewSqliteStore(
cfg.Sqlite, clientConfig.Lnd.ChainParams,
)
case DatabaseBackendPostgres:
log.Infof("Opening postgres database at: %v",
cfg.Postgres.DSN(true))
db, err = loopdb.NewPostgresStore(cfg.Postgres, chainParams)
if err != nil {
return nil, nil, err
}
baseDb = *db.(*loopdb.PostgresStore).BaseDB
db, err = loopdb.NewPostgresStore(
cfg.Postgres, clientConfig.Lnd.ChainParams,
)
default:
return nil, nil, fmt.Errorf("unknown database backend: %s",
cfg.DatabaseBackend)
}
if err != nil {
return nil, nil, fmt.Errorf("unable to open database: %v", err)
}
return db, &baseDb, nil
swapClient, cleanUp, err := loop.NewClient(
cfg.DataDir, db, clientConfig,
)
if err != nil {
return nil, nil, err
}
return swapClient, cleanUp, nil
}
func getLiquidityManager(client *loop.Client) *liquidity.Manager {

@ -8,8 +8,6 @@ import (
"github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/utils"
)
// view prints all swaps currently in the database.
@ -22,25 +20,16 @@ func view(config *Config, lisCfg *ListenerCfg) error {
}
defer lnd.Close()
chainParams, err := network.ChainParams()
if err != nil {
return err
}
swapDb, baseDb, err := openDatabase(config, chainParams)
swapClient, cleanup, err := getClient(config, &lnd.LndServices)
if err != nil {
return err
}
defer cleanup()
sweeperDb := sweepbatcher.NewSQLStore(baseDb, chainParams)
swapClient, cleanup, err := getClient(
config, swapDb, sweeperDb, &lnd.LndServices,
)
chainParams, err := network.ChainParams()
if err != nil {
return err
}
defer cleanup()
if err := viewOut(swapClient, chainParams); err != nil {
return err
@ -62,7 +51,7 @@ func viewOut(swapClient *loop.Client, chainParams *chaincfg.Params) error {
for _, s := range swaps {
s := s
htlc, err := utils.GetHtlc(
htlc, err := loop.GetHtlc(
s.Hash, &s.Contract.SwapContract, chainParams,
)
if err != nil {
@ -113,7 +102,7 @@ func viewIn(swapClient *loop.Client, chainParams *chaincfg.Params) error {
for _, s := range swaps {
s := s
htlc, err := utils.GetHtlc(
htlc, err := loop.GetHtlc(
s.Hash, &s.Contract.SwapContract, chainParams,
)
if err != nil {

@ -65,18 +65,6 @@ type SwapStore interface {
// it's decoding using the proto package's `Unmarshal` method.
FetchLiquidityParams(ctx context.Context) ([]byte, error)
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of
// loop out swaps.
BatchUpdateLoopOutSwapCosts(ctx context.Context,
swaps map[lntypes.Hash]SwapCost) error
// HasMigration returns true if the migration with the given ID has
// been done.
HasMigration(ctx context.Context, migrationID string) (bool, error)
// SetMigration marks the migration with the given ID as done.
SetMigration(ctx context.Context, migrationID string) error
// Close closes the underlying database.
Close() error
}

@ -24,10 +24,6 @@ type LoopOutContract struct {
// DestAddr is the destination address of the loop out swap.
DestAddr btcutil.Address
// IsExternalAddr indicates whether the destination address does not
// belong to the backing lnd node.
IsExternalAddr bool
// SwapInvoice is the invoice that is to be paid by the client to
// initiate the loop out swap.
SwapInvoice string
@ -61,10 +57,6 @@ type LoopOutContract struct {
// allow the server to delay the publication in exchange for possibly
// lower fees.
SwapPublicationDeadline time.Time
// PaymentTimeout is the timeout for any individual off-chain payment
// attempt.
PaymentTimeout time.Duration
}
// ChannelSet stores a set of channels.

@ -32,7 +32,7 @@ const (
ProtocolVersionUserExpiryLoopOut ProtocolVersion = 4
// ProtocolVersionHtlcV2 indicates that the client will use the new
// HTLC v2 scripts for swaps.
// HTLC v2 scrips for swaps.
ProtocolVersionHtlcV2 ProtocolVersion = 5
// ProtocolVersionMultiLoopIn indicates that the client creates a probe

@ -9,7 +9,6 @@ import (
"time"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightningnetwork/lnd/keychain"
@ -32,16 +31,13 @@ func (s *BaseDB) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut,
loopOuts = make([]*LoopOut, len(swaps))
for i, swap := range swaps {
updates, err := s.Queries.GetSwapUpdates(
ctx, swap.SwapHash,
)
updates, err := s.Queries.GetSwapUpdates(ctx, swap.SwapHash)
if err != nil {
return err
}
loopOut, err := ConvertLoopOutRow(
s.network, sqlc.GetLoopOutSwapRow(swap),
updates,
loopOut, err := s.convertLoopOutRow(
sqlc.GetLoopOutSwapRow(swap), updates,
)
if err != nil {
return err
@ -76,8 +72,8 @@ func (s *BaseDB) FetchLoopOutSwap(ctx context.Context,
return err
}
loopOut, err = ConvertLoopOutRow(
s.network, swap, updates,
loopOut, err = s.convertLoopOutRow(
swap, updates,
)
if err != nil {
return err
@ -407,61 +403,6 @@ func (s *BaseDB) BatchInsertUpdate(ctx context.Context,
})
}
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of loop out
// swaps.
func (b *BaseDB) BatchUpdateLoopOutSwapCosts(ctx context.Context,
costs map[lntypes.Hash]SwapCost) error {
writeOpts := &SqliteTxOptions{}
return b.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
for swapHash, cost := range costs {
lastUpdateID, err := tx.GetLastUpdateID(
ctx, swapHash[:],
)
if err != nil {
return err
}
err = tx.OverrideSwapCosts(
ctx, sqlc.OverrideSwapCostsParams{
ID: lastUpdateID,
ServerCost: int64(cost.Server),
OnchainCost: int64(cost.Onchain),
OffchainCost: int64(cost.Offchain),
},
)
if err != nil {
return err
}
}
return nil
})
}
// HasMigration returns true if the migration with the given ID has been done.
func (b *BaseDB) HasMigration(ctx context.Context, migrationID string) (
bool, error) {
migration, err := b.GetMigration(ctx, migrationID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false, err
}
return migration.MigrationTs.Valid, nil
}
// SetMigration marks the migration with the given ID as done.
func (b *BaseDB) SetMigration(ctx context.Context, migrationID string) error {
return b.InsertMigration(ctx, sqlc.InsertMigrationParams{
MigrationID: migrationID,
MigrationTs: sql.NullTime{
Time: time.Now().UTC(),
Valid: true,
},
})
}
// loopToInsertArgs converts a SwapContract struct to the arguments needed to
// insert it into the database.
func loopToInsertArgs(hash lntypes.Hash,
@ -489,7 +430,6 @@ func loopOutToInsertArgs(hash lntypes.Hash,
return sqlc.InsertLoopOutParams{
SwapHash: hash[:],
DestAddress: loopOut.DestAddr.String(),
SingleSweep: loopOut.IsExternalAddr,
SwapInvoice: loopOut.SwapInvoice,
MaxSwapRoutingFee: int64(loopOut.MaxSwapRoutingFee),
SweepConfTarget: loopOut.SweepConfTarget,
@ -498,7 +438,6 @@ func loopOutToInsertArgs(hash lntypes.Hash,
PrepayInvoice: loopOut.PrepayInvoice,
MaxPrepayRoutingFee: int64(loopOut.MaxPrepayRoutingFee),
PublicationDeadline: loopOut.SwapPublicationDeadline.UTC(),
PaymentTimeout: int32(loopOut.PaymentTimeout.Seconds()),
}
}
@ -540,9 +479,9 @@ func swapToHtlcKeysInsertArgs(hash lntypes.Hash,
}
}
// ConvertLoopOutRow converts a database row containing a loop out swap to a
// convertLoopOutRow converts a database row containing a loop out swap to a
// LoopOut struct.
func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
func (s *BaseDB) convertLoopOutRow(row sqlc.GetLoopOutSwapRow,
updates []sqlc.SwapUpdate) (*LoopOut, error) {
htlcKeys, err := fetchHtlcKeys(
@ -559,7 +498,7 @@ func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
return nil, err
}
destAddress, err := btcutil.DecodeAddress(row.DestAddress, network)
destAddress, err := btcutil.DecodeAddress(row.DestAddress, s.network)
if err != nil {
return nil, err
}
@ -584,7 +523,6 @@ func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
ProtocolVersion: ProtocolVersion(row.ProtocolVersion),
},
DestAddr: destAddress,
IsExternalAddr: row.SingleSweep,
SwapInvoice: row.SwapInvoice,
MaxSwapRoutingFee: btcutil.Amount(row.MaxSwapRoutingFee),
SweepConfTarget: row.SweepConfTarget,
@ -592,9 +530,6 @@ func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
PrepayInvoice: row.PrepayInvoice,
MaxPrepayRoutingFee: btcutil.Amount(row.MaxPrepayRoutingFee),
SwapPublicationDeadline: row.PublicationDeadline,
PaymentTimeout: time.Duration(
row.PaymentTimeout,
) * time.Second,
},
Loop: Loop{
Hash: swapHash,
@ -602,7 +537,7 @@ func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
}
if row.OutgoingChanSet != "" {
chanSet, err := ConvertOutgoingChanSet(row.OutgoingChanSet)
chanSet, err := convertOutgoingChanSet(row.OutgoingChanSet)
if err != nil {
return nil, err
}
@ -725,9 +660,9 @@ func getSwapEvents(updates []sqlc.SwapUpdate) ([]*LoopEvent, error) {
return events, nil
}
// ConvertOutgoingChanSet converts a comma separated string of channel IDs into
// convertOutgoingChanSet converts a comma separated string of channel IDs into
// a ChannelSet.
func ConvertOutgoingChanSet(outgoingChanSet string) (ChannelSet, error) {
func convertOutgoingChanSet(outgoingChanSet string) (ChannelSet, error) {
// Split the string into a slice of strings
chanStrings := strings.Split(outgoingChanSet, ",")
channels := make([]uint64, len(chanStrings))

@ -13,7 +13,6 @@ import (
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
)
@ -61,7 +60,6 @@ func TestSqliteLoopOutStore(t *testing.T) {
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: initiationTime,
PaymentTimeout: time.Second * 11,
}
t.Run("no outgoing set", func(t *testing.T) {
@ -122,8 +120,6 @@ func testSqliteLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) {
if expectedState == StatePreimageRevealed {
require.NotNil(t, swap.State().HtlcTxHash)
}
require.Equal(t, time.Second*11, swap.Contract.PaymentTimeout)
}
// If we create a new swap, then it should show up as being initialized
@ -326,15 +322,6 @@ func TestSqliteTypeConversion(t *testing.T) {
func TestIssue615(t *testing.T) {
ctxb := context.Background()
// Create an invoice to get the timestamp from.
invoice := "lnbc5u1pje2dyusp5qs356crpns9u3we8hw7w9gntfz89zkcaxu6w6h6a" +
"pw6jlgc0cynqpp5y2xdzu4eqasuttxp3nrk72vqdzce3wead7nmf693uqpgx" +
"2hd533qdpcyfnx2etyyp3ks6trddjkuueqw3hkketwwv7kgvrd0py95d6vvv" +
"65z0fzxqzfvcqpjrzjqd82srutzjx82prr234anxdlwvs6peklcc92lp9aqs" +
"q296xnwmqd2rrf9gqqtwqqqqqqqqqqqqqqqqqq9q9qxpqysgq768236z7cx6" +
"gyy766wajrmpnpt6wavkf5nypwyj6r3dcxm89aggq2jm2kznaxvr0lrsqgv7" +
"592upfh5ruyrwzy5tethpzere78xfgwqp64jrpa"
// Create a new sqlite store for testing.
sqlDB := NewTestDB(t)
@ -369,7 +356,7 @@ func TestIssue615(t *testing.T) {
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: invoice,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
HtlcConfirmations: 2,
@ -397,138 +384,68 @@ func TestIssue615(t *testing.T) {
require.NoError(t, err)
}
// TestBatchUpdateCost tests that we can batch update the cost of multiple swaps
// at once.
func TestBatchUpdateCost(t *testing.T) {
// Create a new sqlite store for testing.
store := NewTestDB(t)
destAddr := test.GetDestAddr(t, 0)
initiationTime := time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC)
testContract := LoopOutContract{
SwapContract: SwapContract{
AmountRequested: 100,
CltvExpiry: 144,
HtlcKeys: HtlcKeys{
SenderScriptKey: senderKey,
ReceiverScriptKey: receiverKey,
SenderInternalPubKey: senderInternalKey,
ReceiverInternalPubKey: receiverInternalKey,
ClientScriptKeyLocator: keychain.KeyLocator{
Family: 1,
Index: 2,
},
},
MaxMinerFee: 10,
MaxSwapFee: 20,
InitiationHeight: 99,
InitiationTime: initiationTime,
ProtocolVersion: ProtocolVersionMuSig2,
func TestTimeConversions(t *testing.T) {
tests := []struct {
timeString string
expectedTime time.Time
}{
{
timeString: "2018-11-01 00:00:00 +0000 UTC",
expectedTime: time.Date(2018, 11, 1, 0, 0, 0, 0, time.UTC),
},
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: initiationTime,
PaymentTimeout: time.Second * 11,
}
makeSwap := func(preimage lntypes.Preimage) *LoopOutContract {
contract := testContract
contract.Preimage = preimage
return &contract
}
// Next, we'll add two swaps to the database.
preimage1 := testPreimage
preimage2 := lntypes.Preimage{4, 4, 4}
ctxb := context.Background()
swap1 := makeSwap(preimage1)
swap2 := makeSwap(preimage2)
hash1 := swap1.Preimage.Hash()
err := store.CreateLoopOut(ctxb, hash1, swap1)
require.NoError(t, err)
hash2 := swap2.Preimage.Hash()
err = store.CreateLoopOut(ctxb, hash2, swap2)
require.NoError(t, err)
// Add an update to both swaps containing the cost.
err = store.UpdateLoopOut(
ctxb, hash1, testTime,
SwapStateData{
State: StateSuccess,
Cost: SwapCost{
Server: 1,
Onchain: 2,
Offchain: 3,
},
{
timeString: "2018-11-01 00:00:01.10000 +0000 UTC",
expectedTime: time.Date(2018, 11, 1, 0, 0, 1, 100000000, time.UTC),
},
)
require.NoError(t, err)
err = store.UpdateLoopOut(
ctxb, hash2, testTime,
SwapStateData{
State: StateSuccess,
Cost: SwapCost{
Server: 4,
Onchain: 5,
Offchain: 6,
},
{
timeString: "2053-12-29T02:40:44.269009408Z",
expectedTime: time.Date(
time.Now().Year(), 12, 29, 2, 40, 44, 269009408, time.UTC,
),
},
)
require.NoError(t, err)
updateMap := map[lntypes.Hash]SwapCost{
hash1: {
Server: 2,
Onchain: 3,
Offchain: 4,
{
timeString: "55563-06-27 02:09:24 +0000 UTC",
expectedTime: time.Date(
time.Now().Year(), 6, 27, 2, 9, 24, 0, time.UTC,
),
},
{
timeString: "2172-03-11 10:01:11.849906176 +0000 UTC",
expectedTime: time.Date(
time.Now().Year(), 3, 11, 10, 1, 11, 849906176, time.UTC,
),
},
{
timeString: "2023-08-04 16:07:49 +0800 CST",
expectedTime: time.Date(
2023, 8, 4, 8, 7, 49, 0, time.UTC,
),
},
{
timeString: "2023-08-04 16:07:49 -0700 MST",
expectedTime: time.Date(
2023, 8, 4, 23, 7, 49, 0, time.UTC,
),
},
{
timeString: "2023-08-04T16:07:49+08:00",
expectedTime: time.Date(
2023, 8, 4, 8, 7, 49, 0, time.UTC,
),
},
hash2: {
Server: 6,
Onchain: 7,
Offchain: 8,
{
timeString: "2023-08-04T16:07:49+08:00",
expectedTime: time.Date(
2023, 8, 4, 8, 7, 49, 0, time.UTC,
),
},
}
require.NoError(t, store.BatchUpdateLoopOutSwapCosts(ctxb, updateMap))
swaps, err := store.FetchLoopOutSwaps(ctxb)
require.NoError(t, err)
require.Len(t, swaps, 2)
swapsMap := make(map[lntypes.Hash]*LoopOut)
swapsMap[swaps[0].Hash] = swaps[0]
swapsMap[swaps[1].Hash] = swaps[1]
require.Equal(t, updateMap[hash1], swapsMap[hash1].State().Cost)
require.Equal(t, updateMap[hash2], swapsMap[hash2].State().Cost)
}
// TestMigrationTracker tests the migration tracker functionality.
func TestMigrationTracker(t *testing.T) {
ctxb := context.Background()
// Create a new sqlite store for testing.
sqlDB := NewTestDB(t)
hasMigration, err := sqlDB.HasMigration(ctxb, "test")
require.NoError(t, err)
require.False(t, hasMigration)
require.NoError(t, sqlDB.SetMigration(ctxb, "test"))
hasMigration, err = sqlDB.HasMigration(ctxb, "test")
require.NoError(t, err)
require.True(t, hasMigration)
for _, test := range tests {
time, err := fixTimeStamp(test.timeString)
require.NoError(t, err)
require.Equal(t, test.expectedTime, time)
}
}
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

@ -1,296 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: batch.sql
package sqlc
import (
"context"
"database/sql"
)
const confirmBatch = `-- name: ConfirmBatch :exec
UPDATE
sweep_batches
SET
confirmed = TRUE
WHERE
id = $1
`
func (q *Queries) ConfirmBatch(ctx context.Context, id int32) error {
_, err := q.db.ExecContext(ctx, confirmBatch, id)
return err
}
const dropBatch = `-- name: DropBatch :exec
DELETE FROM sweep_batches WHERE id = $1
`
func (q *Queries) DropBatch(ctx context.Context, id int32) error {
_, err := q.db.ExecContext(ctx, dropBatch, id)
return err
}
const getBatchSweeps = `-- name: GetBatchSweeps :many
SELECT
id, swap_hash, batch_id, outpoint_txid, outpoint_index, amt, completed
FROM
sweeps
WHERE
batch_id = $1
ORDER BY
id ASC
`
func (q *Queries) GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error) {
rows, err := q.db.QueryContext(ctx, getBatchSweeps, batchID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Sweep
for rows.Next() {
var i Sweep
if err := rows.Scan(
&i.ID,
&i.SwapHash,
&i.BatchID,
&i.OutpointTxid,
&i.OutpointIndex,
&i.Amt,
&i.Completed,
); 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 getBatchSweptAmount = `-- name: GetBatchSweptAmount :one
SELECT
SUM(amt) AS total
FROM
sweeps
WHERE
batch_id = $1
AND
completed = TRUE
`
func (q *Queries) GetBatchSweptAmount(ctx context.Context, batchID int32) (int64, error) {
row := q.db.QueryRowContext(ctx, getBatchSweptAmount, batchID)
var total int64
err := row.Scan(&total)
return total, err
}
const getParentBatch = `-- name: GetParentBatch :one
SELECT
sweep_batches.id, sweep_batches.confirmed, sweep_batches.batch_tx_id, sweep_batches.batch_pk_script, sweep_batches.last_rbf_height, sweep_batches.last_rbf_sat_per_kw, sweep_batches.max_timeout_distance
FROM
sweep_batches
JOIN
sweeps ON sweep_batches.id = sweeps.batch_id
WHERE
sweeps.swap_hash = $1
AND
sweeps.completed = TRUE
AND
sweep_batches.confirmed = TRUE
`
func (q *Queries) GetParentBatch(ctx context.Context, swapHash []byte) (SweepBatch, error) {
row := q.db.QueryRowContext(ctx, getParentBatch, swapHash)
var i SweepBatch
err := row.Scan(
&i.ID,
&i.Confirmed,
&i.BatchTxID,
&i.BatchPkScript,
&i.LastRbfHeight,
&i.LastRbfSatPerKw,
&i.MaxTimeoutDistance,
)
return i, err
}
const getSweepStatus = `-- name: GetSweepStatus :one
SELECT
COALESCE(s.completed, f.false_value) AS completed
FROM
(SELECT false AS false_value) AS f
LEFT JOIN
sweeps s ON s.swap_hash = $1
`
func (q *Queries) GetSweepStatus(ctx context.Context, swapHash []byte) (bool, error) {
row := q.db.QueryRowContext(ctx, getSweepStatus, swapHash)
var completed bool
err := row.Scan(&completed)
return completed, err
}
const getUnconfirmedBatches = `-- name: GetUnconfirmedBatches :many
SELECT
id, confirmed, batch_tx_id, batch_pk_script, last_rbf_height, last_rbf_sat_per_kw, max_timeout_distance
FROM
sweep_batches
WHERE
confirmed = FALSE
`
func (q *Queries) GetUnconfirmedBatches(ctx context.Context) ([]SweepBatch, error) {
rows, err := q.db.QueryContext(ctx, getUnconfirmedBatches)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SweepBatch
for rows.Next() {
var i SweepBatch
if err := rows.Scan(
&i.ID,
&i.Confirmed,
&i.BatchTxID,
&i.BatchPkScript,
&i.LastRbfHeight,
&i.LastRbfSatPerKw,
&i.MaxTimeoutDistance,
); 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 insertBatch = `-- name: InsertBatch :one
INSERT INTO sweep_batches (
confirmed,
batch_tx_id,
batch_pk_script,
last_rbf_height,
last_rbf_sat_per_kw,
max_timeout_distance
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) RETURNING id
`
type InsertBatchParams struct {
Confirmed bool
BatchTxID sql.NullString
BatchPkScript []byte
LastRbfHeight sql.NullInt32
LastRbfSatPerKw sql.NullInt32
MaxTimeoutDistance int32
}
func (q *Queries) InsertBatch(ctx context.Context, arg InsertBatchParams) (int32, error) {
row := q.db.QueryRowContext(ctx, insertBatch,
arg.Confirmed,
arg.BatchTxID,
arg.BatchPkScript,
arg.LastRbfHeight,
arg.LastRbfSatPerKw,
arg.MaxTimeoutDistance,
)
var id int32
err := row.Scan(&id)
return id, err
}
const updateBatch = `-- name: UpdateBatch :exec
UPDATE sweep_batches SET
confirmed = $2,
batch_tx_id = $3,
batch_pk_script = $4,
last_rbf_height = $5,
last_rbf_sat_per_kw = $6
WHERE id = $1
`
type UpdateBatchParams struct {
ID int32
Confirmed bool
BatchTxID sql.NullString
BatchPkScript []byte
LastRbfHeight sql.NullInt32
LastRbfSatPerKw sql.NullInt32
}
func (q *Queries) UpdateBatch(ctx context.Context, arg UpdateBatchParams) error {
_, err := q.db.ExecContext(ctx, updateBatch,
arg.ID,
arg.Confirmed,
arg.BatchTxID,
arg.BatchPkScript,
arg.LastRbfHeight,
arg.LastRbfSatPerKw,
)
return err
}
const upsertSweep = `-- name: UpsertSweep :exec
INSERT INTO sweeps (
swap_hash,
batch_id,
outpoint_txid,
outpoint_index,
amt,
completed
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) ON CONFLICT (swap_hash) DO UPDATE SET
batch_id = $2,
outpoint_txid = $3,
outpoint_index = $4,
amt = $5,
completed = $6
`
type UpsertSweepParams struct {
SwapHash []byte
BatchID int32
OutpointTxid []byte
OutpointIndex int32
Amt int64
Completed bool
}
func (q *Queries) UpsertSweep(ctx context.Context, arg UpsertSweepParams) error {
_, err := q.db.ExecContext(ctx, upsertSweep,
arg.SwapHash,
arg.BatchID,
arg.OutpointTxid,
arg.OutpointIndex,
arg.Amt,
arg.Completed,
)
return err
}

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.17.2
package sqlc

@ -1,329 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: instantout.sql
package sqlc
import (
"context"
"database/sql"
"time"
)
const getInstantOutSwap = `-- name: GetInstantOutSwap :one
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
instantout_swaps.swap_hash, instantout_swaps.preimage, instantout_swaps.sweep_address, instantout_swaps.outgoing_chan_set, instantout_swaps.htlc_fee_rate, instantout_swaps.reservation_ids, instantout_swaps.swap_invoice, instantout_swaps.finalized_htlc_tx, instantout_swaps.sweep_tx_hash, instantout_swaps.finalized_sweepless_sweep_tx, instantout_swaps.sweep_confirmation_height,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM
swaps
JOIN
instantout_swaps ON swaps.swap_hash = instantout_swaps.swap_hash
JOIN
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
WHERE
swaps.swap_hash = $1
`
type GetInstantOutSwapRow struct {
ID int32
SwapHash []byte
Preimage []byte
InitiationTime time.Time
AmountRequested int64
CltvExpiry int32
MaxMinerFee int64
MaxSwapFee int64
InitiationHeight int32
ProtocolVersion int32
Label string
SwapHash_2 []byte
Preimage_2 []byte
SweepAddress string
OutgoingChanSet string
HtlcFeeRate int64
ReservationIds []byte
SwapInvoice string
FinalizedHtlcTx []byte
SweepTxHash []byte
FinalizedSweeplessSweepTx []byte
SweepConfirmationHeight sql.NullInt32
SwapHash_3 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
SenderInternalPubkey []byte
ReceiverInternalPubkey []byte
ClientKeyFamily int32
ClientKeyIndex int32
}
func (q *Queries) GetInstantOutSwap(ctx context.Context, swapHash []byte) (GetInstantOutSwapRow, error) {
row := q.db.QueryRowContext(ctx, getInstantOutSwap, swapHash)
var i GetInstantOutSwapRow
err := row.Scan(
&i.ID,
&i.SwapHash,
&i.Preimage,
&i.InitiationTime,
&i.AmountRequested,
&i.CltvExpiry,
&i.MaxMinerFee,
&i.MaxSwapFee,
&i.InitiationHeight,
&i.ProtocolVersion,
&i.Label,
&i.SwapHash_2,
&i.Preimage_2,
&i.SweepAddress,
&i.OutgoingChanSet,
&i.HtlcFeeRate,
&i.ReservationIds,
&i.SwapInvoice,
&i.FinalizedHtlcTx,
&i.SweepTxHash,
&i.FinalizedSweeplessSweepTx,
&i.SweepConfirmationHeight,
&i.SwapHash_3,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
&i.SenderInternalPubkey,
&i.ReceiverInternalPubkey,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
)
return i, err
}
const getInstantOutSwapUpdates = `-- name: GetInstantOutSwapUpdates :many
SELECT
instantout_updates.id, instantout_updates.swap_hash, instantout_updates.update_state, instantout_updates.update_timestamp
FROM
instantout_updates
WHERE
instantout_updates.swap_hash = $1
`
func (q *Queries) GetInstantOutSwapUpdates(ctx context.Context, swapHash []byte) ([]InstantoutUpdate, error) {
rows, err := q.db.QueryContext(ctx, getInstantOutSwapUpdates, swapHash)
if err != nil {
return nil, err
}
defer rows.Close()
var items []InstantoutUpdate
for rows.Next() {
var i InstantoutUpdate
if err := rows.Scan(
&i.ID,
&i.SwapHash,
&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 getInstantOutSwaps = `-- name: GetInstantOutSwaps :many
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
instantout_swaps.swap_hash, instantout_swaps.preimage, instantout_swaps.sweep_address, instantout_swaps.outgoing_chan_set, instantout_swaps.htlc_fee_rate, instantout_swaps.reservation_ids, instantout_swaps.swap_invoice, instantout_swaps.finalized_htlc_tx, instantout_swaps.sweep_tx_hash, instantout_swaps.finalized_sweepless_sweep_tx, instantout_swaps.sweep_confirmation_height,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM
swaps
JOIN
instantout_swaps ON swaps.swap_hash = instantout_swaps.swap_hash
JOIN
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
ORDER BY
swaps.id
`
type GetInstantOutSwapsRow struct {
ID int32
SwapHash []byte
Preimage []byte
InitiationTime time.Time
AmountRequested int64
CltvExpiry int32
MaxMinerFee int64
MaxSwapFee int64
InitiationHeight int32
ProtocolVersion int32
Label string
SwapHash_2 []byte
Preimage_2 []byte
SweepAddress string
OutgoingChanSet string
HtlcFeeRate int64
ReservationIds []byte
SwapInvoice string
FinalizedHtlcTx []byte
SweepTxHash []byte
FinalizedSweeplessSweepTx []byte
SweepConfirmationHeight sql.NullInt32
SwapHash_3 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
SenderInternalPubkey []byte
ReceiverInternalPubkey []byte
ClientKeyFamily int32
ClientKeyIndex int32
}
func (q *Queries) GetInstantOutSwaps(ctx context.Context) ([]GetInstantOutSwapsRow, error) {
rows, err := q.db.QueryContext(ctx, getInstantOutSwaps)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetInstantOutSwapsRow
for rows.Next() {
var i GetInstantOutSwapsRow
if err := rows.Scan(
&i.ID,
&i.SwapHash,
&i.Preimage,
&i.InitiationTime,
&i.AmountRequested,
&i.CltvExpiry,
&i.MaxMinerFee,
&i.MaxSwapFee,
&i.InitiationHeight,
&i.ProtocolVersion,
&i.Label,
&i.SwapHash_2,
&i.Preimage_2,
&i.SweepAddress,
&i.OutgoingChanSet,
&i.HtlcFeeRate,
&i.ReservationIds,
&i.SwapInvoice,
&i.FinalizedHtlcTx,
&i.SweepTxHash,
&i.FinalizedSweeplessSweepTx,
&i.SweepConfirmationHeight,
&i.SwapHash_3,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
&i.SenderInternalPubkey,
&i.ReceiverInternalPubkey,
&i.ClientKeyFamily,
&i.ClientKeyIndex,
); 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 insertInstantOut = `-- name: InsertInstantOut :exec
INSERT INTO instantout_swaps (
swap_hash,
preimage,
sweep_address,
outgoing_chan_set,
htlc_fee_rate,
reservation_ids,
swap_invoice
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
)
`
type InsertInstantOutParams struct {
SwapHash []byte
Preimage []byte
SweepAddress string
OutgoingChanSet string
HtlcFeeRate int64
ReservationIds []byte
SwapInvoice string
}
func (q *Queries) InsertInstantOut(ctx context.Context, arg InsertInstantOutParams) error {
_, err := q.db.ExecContext(ctx, insertInstantOut,
arg.SwapHash,
arg.Preimage,
arg.SweepAddress,
arg.OutgoingChanSet,
arg.HtlcFeeRate,
arg.ReservationIds,
arg.SwapInvoice,
)
return err
}
const insertInstantOutUpdate = `-- name: InsertInstantOutUpdate :exec
INSERT INTO instantout_updates (
swap_hash,
update_state,
update_timestamp
) VALUES (
$1,
$2,
$3
)
`
type InsertInstantOutUpdateParams struct {
SwapHash []byte
UpdateState string
UpdateTimestamp time.Time
}
func (q *Queries) InsertInstantOutUpdate(ctx context.Context, arg InsertInstantOutUpdateParams) error {
_, err := q.db.ExecContext(ctx, insertInstantOutUpdate, arg.SwapHash, arg.UpdateState, arg.UpdateTimestamp)
return err
}
const updateInstantOut = `-- name: UpdateInstantOut :exec
UPDATE instantout_swaps
SET
finalized_htlc_tx = $2,
sweep_tx_hash = $3,
finalized_sweepless_sweep_tx = $4,
sweep_confirmation_height = $5
WHERE
instantout_swaps.swap_hash = $1
`
type UpdateInstantOutParams struct {
SwapHash []byte
FinalizedHtlcTx []byte
SweepTxHash []byte
FinalizedSweeplessSweepTx []byte
SweepConfirmationHeight sql.NullInt32
}
func (q *Queries) UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error {
_, err := q.db.ExecContext(ctx, updateInstantOut,
arg.SwapHash,
arg.FinalizedHtlcTx,
arg.SweepTxHash,
arg.FinalizedSweeplessSweepTx,
arg.SweepConfirmationHeight,
)
return err
}

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.17.2
// source: liquidity_params.sql
package sqlc

@ -1,45 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// source: migration_tracker.sql
package sqlc
import (
"context"
"database/sql"
)
const getMigration = `-- name: GetMigration :one
SELECT
migration_id,
migration_ts
FROM
migration_tracker
WHERE
migration_id = $1
`
func (q *Queries) GetMigration(ctx context.Context, migrationID string) (MigrationTracker, error) {
row := q.db.QueryRowContext(ctx, getMigration, migrationID)
var i MigrationTracker
err := row.Scan(&i.MigrationID, &i.MigrationTs)
return i, err
}
const insertMigration = `-- name: InsertMigration :exec
INSERT INTO migration_tracker (
migration_id,
migration_ts
) VALUES ($1, $2)
`
type InsertMigrationParams struct {
MigrationID string
MigrationTs sql.NullTime
}
func (q *Queries) InsertMigration(ctx context.Context, arg InsertMigrationParams) error {
_, err := q.db.ExecContext(ctx, insertMigration, arg.MigrationID, arg.MigrationTs)
return err
}

@ -1,2 +0,0 @@
DROP TABLE IF EXISTS reservation_updates;
DROP TABLE IF EXISTS reservations;

@ -1,56 +0,0 @@
-- 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
);

@ -1 +0,0 @@
ALTER TABLE loopout_swaps DROP COLUMN single_sweep;

@ -1,4 +0,0 @@
-- is_external_addr indicates whether the destination address of the swap is not
-- a wallet address. The default value used is TRUE in order to maintain the old
-- behavior of swaps which doesn't override the destination address.
ALTER TABLE loopout_swaps ADD single_sweep BOOLEAN NOT NULL DEFAULT TRUE;

@ -1,2 +0,0 @@
DROP TABLE sweep_batches;
DROP TABLE sweeps;

@ -1,58 +0,0 @@
-- sweep_batches stores the on-going swaps that are batched together.
CREATE TABLE sweep_batches (
-- id is the autoincrementing primary key of the batch.
id INTEGER PRIMARY KEY,
-- confirmed indicates whether this batch is confirmed.
confirmed BOOLEAN NOT NULL DEFAULT FALSE,
-- batch_tx_id is the transaction id of the batch transaction.
batch_tx_id TEXT,
-- batch_pk_script is the pkscript of the batch transaction's output.
batch_pk_script BLOB,
-- last_rbf_height was the last height at which we attempted to publish
-- an rbf replacement transaction.
last_rbf_height INTEGER,
-- last_rbf_sat_per_kw was the last sat per kw fee rate we used for the
-- last published transaction.
last_rbf_sat_per_kw INTEGER,
-- max_timeout_distance is the maximum distance the timeouts of the
-- sweeps can have in the batch.
max_timeout_distance INTEGER NOT NULL
);
-- sweeps stores the individual sweeps that are part of a batch.
CREATE TABLE sweeps (
-- id is the autoincrementing primary key.
id INTEGER PRIMARY KEY,
-- swap_hash is the hash of the swap that is being swept.
swap_hash BLOB NOT NULL UNIQUE,
-- batch_id is the id of the batch this swap is part of.
batch_id INTEGER NOT NULL,
-- outpoint_txid is the transaction id of the output being swept.
outpoint_txid BLOB NOT NULL,
-- outpoint_index is the index of the output being swept.
outpoint_index INTEGER NOT NULL,
-- amt is the amount of the output being swept.
amt BIGINT NOT NULL,
-- completed indicates whether the sweep has been completed.
completed BOOLEAN NOT NULL DEFAULT FALSE,
-- Foreign key constraint to ensure that we reference an existing batch
-- id.
FOREIGN KEY (batch_id) REFERENCES sweep_batches(id),
-- Foreign key constraint to ensure that swap_hash references an
-- existing swap.
FOREIGN KEY (swap_hash) REFERENCES swaps(swap_hash)
);

@ -1,4 +0,0 @@
DROP INDEX IF EXISTS instantout_updates_swap_hash_idx;
DROP INDEX IF EXISTS instantout_swap_hash_idx;
DROP TABLE IF EXISTS instantout_updates;
DROP TABLE IF EXISTS instantout_swaps;

@ -1,52 +0,0 @@
CREATE TABLE IF NOT EXISTS instantout_swaps (
-- swap_hash points to the parent swap hash.
swap_hash BLOB PRIMARY KEY,
-- preimage is the preimage of the swap.
preimage BLOB NOT NULL,
-- sweep_address is the address that the server should sweep the funds to.
sweep_address TEXT NOT NULL,
-- outgoing_chan_set is the set of short ids of channels that may be used.
-- If empty, any channel may be used.
outgoing_chan_set TEXT NOT NULL,
-- htlc_fee_rate is the fee rate in sat/kw that is used for the htlc transaction.
htlc_fee_rate BIGINT NOT NULL,
-- reservation_ids is a list of ids of the reservations that are used for this swap.
reservation_ids BLOB NOT NULL,
-- swap_invoice is the invoice that is to be paid by the client to
-- initiate the loop out swap.
swap_invoice TEXT NOT NULL,
-- finalized_htlc_tx contains the fully signed htlc transaction.
finalized_htlc_tx BLOB,
-- sweep_tx_hash is the hash of the transaction that sweeps the htlc.
sweep_tx_hash BLOB,
-- finalized_sweepless_sweep_tx contains the fully signed sweepless sweep transaction.
finalized_sweepless_sweep_tx BLOB,
-- sweep_confirmation_height is the block height at which the sweep transaction is confirmed.
sweep_confirmation_height INTEGER
);
CREATE TABLE IF NOT EXISTS instantout_updates (
-- id is auto incremented for each update.
id INTEGER PRIMARY KEY,
-- swap_hash is the hash of the swap that this update is for.
swap_hash BLOB NOT NULL REFERENCES instantout_swaps(swap_hash),
-- update_state is the state of the swap at the time of the update.
update_state TEXT NOT NULL,
-- update_timestamp is the time at which the update was created.
update_timestamp TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS instantout_updates_swap_hash_idx ON instantout_updates(swap_hash);

@ -1,3 +0,0 @@
-- payment_timeout is the timeout in seconds for each individual off-chain
-- payment.
ALTER TABLE loopout_swaps DROP COLUMN payment_timeout;

@ -1,3 +0,0 @@
-- payment_timeout is the timeout in seconds for each individual off-chain
-- payment.
ALTER TABLE loopout_swaps ADD payment_timeout INTEGER NOT NULL DEFAULT 0;

@ -1,9 +0,0 @@
CREATE TABLE migration_tracker (
-- migration_id is the id of the migration.
migration_id TEXT NOT NULL,
-- migration_ts is the timestamp at which the migration was run.
migration_ts TIMESTAMP,
PRIMARY KEY (migration_id)
);

@ -1,11 +1,10 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.17.2
package sqlc
import (
"database/sql"
"time"
)
@ -19,27 +18,6 @@ type HtlcKey struct {
ClientKeyIndex int32
}
type InstantoutSwap struct {
SwapHash []byte
Preimage []byte
SweepAddress string
OutgoingChanSet string
HtlcFeeRate int64
ReservationIds []byte
SwapInvoice string
FinalizedHtlcTx []byte
SweepTxHash []byte
FinalizedSweeplessSweepTx []byte
SweepConfirmationHeight sql.NullInt32
}
type InstantoutUpdate struct {
ID int32
SwapHash []byte
UpdateState string
UpdateTimestamp time.Time
}
type LiquidityParam struct {
ID int32
Params []byte
@ -63,35 +41,6 @@ type LoopoutSwap struct {
PrepayInvoice string
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
}
type MigrationTracker struct {
MigrationID string
MigrationTs sql.NullTime
}
type Reservation struct {
ID int32
ReservationID []byte
ClientPubkey []byte
ServerPubkey []byte
Expiry int32
Value int64
ClientKeyFamily int32
ClientKeyIndex int32
InitiationHeight int32
TxHash []byte
OutIndex sql.NullInt32
ConfirmationHeight sql.NullInt32
}
type ReservationUpdate struct {
ID int32
ReservationID []byte
UpdateState string
UpdateTimestamp time.Time
}
type Swap struct {
@ -118,23 +67,3 @@ type SwapUpdate struct {
OnchainCost int64
OffchainCost int64
}
type Sweep struct {
ID int32
SwapHash []byte
BatchID int32
OutpointTxid []byte
OutpointIndex int32
Amt int64
Completed bool
}
type SweepBatch struct {
ID int32
Confirmed bool
BatchTxID sql.NullString
BatchPkScript []byte
LastRbfHeight sql.NullInt32
LastRbfSatPerKw sql.NullInt32
MaxTimeoutDistance int32
}

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.17.2
package sqlc
@ -9,44 +9,18 @@ import (
)
type Querier interface {
ConfirmBatch(ctx context.Context, id int32) error
CreateReservation(ctx context.Context, arg CreateReservationParams) error
DropBatch(ctx context.Context, id int32) error
FetchLiquidityParams(ctx context.Context) ([]byte, error)
GetBatchSweeps(ctx context.Context, batchID int32) ([]Sweep, error)
GetBatchSweptAmount(ctx context.Context, batchID int32) (int64, error)
GetInstantOutSwap(ctx context.Context, swapHash []byte) (GetInstantOutSwapRow, error)
GetInstantOutSwapUpdates(ctx context.Context, swapHash []byte) ([]InstantoutUpdate, error)
GetInstantOutSwaps(ctx context.Context) ([]GetInstantOutSwapsRow, error)
GetLastUpdateID(ctx context.Context, swapHash []byte) (int32, error)
GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopInSwapRow, error)
GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, error)
GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopOutSwapRow, error)
GetLoopOutSwaps(ctx context.Context) ([]GetLoopOutSwapsRow, error)
GetMigration(ctx context.Context, migrationID string) (MigrationTracker, error)
GetParentBatch(ctx context.Context, swapHash []byte) (SweepBatch, error)
GetReservation(ctx context.Context, reservationID []byte) (Reservation, error)
GetReservationUpdates(ctx context.Context, reservationID []byte) ([]ReservationUpdate, error)
GetReservations(ctx context.Context) ([]Reservation, error)
GetSwapUpdates(ctx context.Context, swapHash []byte) ([]SwapUpdate, error)
GetSweepStatus(ctx context.Context, swapHash []byte) (bool, error)
GetUnconfirmedBatches(ctx context.Context) ([]SweepBatch, error)
InsertBatch(ctx context.Context, arg InsertBatchParams) (int32, error)
InsertHtlcKeys(ctx context.Context, arg InsertHtlcKeysParams) error
InsertInstantOut(ctx context.Context, arg InsertInstantOutParams) error
InsertInstantOutUpdate(ctx context.Context, arg InsertInstantOutUpdateParams) error
InsertLoopIn(ctx context.Context, arg InsertLoopInParams) error
InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error
InsertMigration(ctx context.Context, arg InsertMigrationParams) error
InsertReservationUpdate(ctx context.Context, arg InsertReservationUpdateParams) error
InsertSwap(ctx context.Context, arg InsertSwapParams) error
InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdateParams) error
OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error
UpdateBatch(ctx context.Context, arg UpdateBatchParams) error
UpdateInstantOut(ctx context.Context, arg UpdateInstantOutParams) error
UpdateReservation(ctx context.Context, arg UpdateReservationParams) error
UpsertLiquidityParams(ctx context.Context, params []byte) error
UpsertSweep(ctx context.Context, arg UpsertSweepParams) error
}
var _ Querier = (*Queries)(nil)

@ -1,108 +0,0 @@
-- name: GetUnconfirmedBatches :many
SELECT
*
FROM
sweep_batches
WHERE
confirmed = FALSE;
-- name: InsertBatch :one
INSERT INTO sweep_batches (
confirmed,
batch_tx_id,
batch_pk_script,
last_rbf_height,
last_rbf_sat_per_kw,
max_timeout_distance
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) RETURNING id;
-- name: DropBatch :exec
DELETE FROM sweep_batches WHERE id = $1;
-- name: UpdateBatch :exec
UPDATE sweep_batches SET
confirmed = $2,
batch_tx_id = $3,
batch_pk_script = $4,
last_rbf_height = $5,
last_rbf_sat_per_kw = $6
WHERE id = $1;
-- name: ConfirmBatch :exec
UPDATE
sweep_batches
SET
confirmed = TRUE
WHERE
id = $1;
-- name: UpsertSweep :exec
INSERT INTO sweeps (
swap_hash,
batch_id,
outpoint_txid,
outpoint_index,
amt,
completed
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) ON CONFLICT (swap_hash) DO UPDATE SET
batch_id = $2,
outpoint_txid = $3,
outpoint_index = $4,
amt = $5,
completed = $6;
-- name: GetParentBatch :one
SELECT
sweep_batches.*
FROM
sweep_batches
JOIN
sweeps ON sweep_batches.id = sweeps.batch_id
WHERE
sweeps.swap_hash = $1
AND
sweeps.completed = TRUE
AND
sweep_batches.confirmed = TRUE;
-- name: GetBatchSweptAmount :one
SELECT
SUM(amt) AS total
FROM
sweeps
WHERE
batch_id = $1
AND
completed = TRUE;
-- name: GetBatchSweeps :many
SELECT
*
FROM
sweeps
WHERE
batch_id = $1
ORDER BY
id ASC;
-- name: GetSweepStatus :one
SELECT
COALESCE(s.completed, f.false_value) AS completed
FROM
(SELECT false AS false_value) AS f
LEFT JOIN
sweeps s ON s.swap_hash = $1;

@ -1,75 +0,0 @@
-- name: InsertInstantOut :exec
INSERT INTO instantout_swaps (
swap_hash,
preimage,
sweep_address,
outgoing_chan_set,
htlc_fee_rate,
reservation_ids,
swap_invoice
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
);
-- name: UpdateInstantOut :exec
UPDATE instantout_swaps
SET
finalized_htlc_tx = $2,
sweep_tx_hash = $3,
finalized_sweepless_sweep_tx = $4,
sweep_confirmation_height = $5
WHERE
instantout_swaps.swap_hash = $1;
-- name: InsertInstantOutUpdate :exec
INSERT INTO instantout_updates (
swap_hash,
update_state,
update_timestamp
) VALUES (
$1,
$2,
$3
);
-- name: GetInstantOutSwap :one
SELECT
swaps.*,
instantout_swaps.*,
htlc_keys.*
FROM
swaps
JOIN
instantout_swaps ON swaps.swap_hash = instantout_swaps.swap_hash
JOIN
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
WHERE
swaps.swap_hash = $1;
-- name: GetInstantOutSwaps :many
SELECT
swaps.*,
instantout_swaps.*,
htlc_keys.*
FROM
swaps
JOIN
instantout_swaps ON swaps.swap_hash = instantout_swaps.swap_hash
JOIN
htlc_keys ON swaps.swap_hash = htlc_keys.swap_hash
ORDER BY
swaps.id;
-- name: GetInstantOutSwapUpdates :many
SELECT
instantout_updates.*
FROM
instantout_updates
WHERE
instantout_updates.swap_hash = $1;

@ -1,14 +0,0 @@
-- name: InsertMigration :exec
INSERT INTO migration_tracker (
migration_id,
migration_ts
) VALUES ($1, $2);
-- name: GetMigration :one
SELECT
migration_id,
migration_ts
FROM
migration_tracker
WHERE
migration_id = $1;

@ -1,66 +0,0 @@
-- 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;

@ -1,9 +1,9 @@
-- name: GetLoopOutSwaps :many
SELECT
SELECT
swaps.*,
loopout_swaps.*,
htlc_keys.*
FROM
FROM
swaps
JOIN
loopout_swaps ON swaps.swap_hash = loopout_swaps.swap_hash
@ -13,7 +13,7 @@ ORDER BY
swaps.id;
-- name: GetLoopOutSwap :one
SELECT
SELECT
swaps.*,
loopout_swaps.*,
htlc_keys.*
@ -27,7 +27,7 @@ WHERE
swaps.swap_hash = $1;
-- name: GetLoopInSwaps :many
SELECT
SELECT
swaps.*,
loopin_swaps.*,
htlc_keys.*
@ -41,7 +41,7 @@ ORDER BY
swaps.id;
-- name: GetLoopInSwap :one
SELECT
SELECT
swaps.*,
loopin_swaps.*,
htlc_keys.*
@ -55,7 +55,7 @@ WHERE
swaps.swap_hash = $1;
-- name: GetSwapUpdates :many
SELECT
SELECT
*
FROM
swap_updates
@ -104,11 +104,9 @@ INSERT INTO loopout_swaps (
outgoing_chan_set,
prepay_invoice,
max_prepay_routing_fee,
publication_deadline,
single_sweep,
payment_timeout
publication_deadline
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
);
-- name: InsertLoopIn :exec
@ -132,20 +130,4 @@ INSERT INTO htlc_keys(
client_key_index
) VALUES (
$1, $2, $3, $4, $5, $6, $7
);
-- name: GetLastUpdateID :one
SELECT id
FROM swap_updates
WHERE swap_hash = $1
ORDER BY update_timestamp DESC
LIMIT 1;
-- name: OverrideSwapCosts :exec
UPDATE swap_updates
SET
server_cost = $2,
onchain_cost = $3,
offchain_cost = $4
WHERE id = $1;
);

@ -1,222 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// 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,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.25.0
// sqlc v1.17.2
// source: swaps.sql
package sqlc
@ -10,23 +10,8 @@ import (
"time"
)
const getLastUpdateID = `-- name: GetLastUpdateID :one
SELECT id
FROM swap_updates
WHERE swap_hash = $1
ORDER BY update_timestamp DESC
LIMIT 1
`
func (q *Queries) GetLastUpdateID(ctx context.Context, swapHash []byte) (int32, error) {
row := q.db.QueryRowContext(ctx, getLastUpdateID, swapHash)
var id int32
err := row.Scan(&id)
return id, err
}
const getLoopInSwap = `-- name: GetLoopInSwap :one
SELECT
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopin_swaps.swap_hash, loopin_swaps.htlc_conf_target, loopin_swaps.last_hop, loopin_swaps.external_htlc,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
@ -96,7 +81,7 @@ func (q *Queries) GetLoopInSwap(ctx context.Context, swapHash []byte) (GetLoopIn
}
const getLoopInSwaps = `-- name: GetLoopInSwaps :many
SELECT
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopin_swaps.swap_hash, loopin_swaps.htlc_conf_target, loopin_swaps.last_hop, loopin_swaps.external_htlc,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
@ -182,9 +167,9 @@ func (q *Queries) GetLoopInSwaps(ctx context.Context) ([]GetLoopInSwapsRow, erro
}
const getLoopOutSwap = `-- name: GetLoopOutSwap :one
SELECT
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep, loopout_swaps.payment_timeout,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM
swaps
@ -218,8 +203,6 @@ type GetLoopOutSwapRow struct {
PrepayInvoice string
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
SwapHash_3 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
@ -254,8 +237,6 @@ func (q *Queries) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopO
&i.PrepayInvoice,
&i.MaxPrepayRoutingFee,
&i.PublicationDeadline,
&i.SingleSweep,
&i.PaymentTimeout,
&i.SwapHash_3,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
@ -268,11 +249,11 @@ func (q *Queries) GetLoopOutSwap(ctx context.Context, swapHash []byte) (GetLoopO
}
const getLoopOutSwaps = `-- name: GetLoopOutSwaps :many
SELECT
SELECT
swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline, loopout_swaps.single_sweep, loopout_swaps.payment_timeout,
loopout_swaps.swap_hash, loopout_swaps.dest_address, loopout_swaps.swap_invoice, loopout_swaps.max_swap_routing_fee, loopout_swaps.sweep_conf_target, loopout_swaps.htlc_confirmations, loopout_swaps.outgoing_chan_set, loopout_swaps.prepay_invoice, loopout_swaps.max_prepay_routing_fee, loopout_swaps.publication_deadline,
htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index
FROM
FROM
swaps
JOIN
loopout_swaps ON swaps.swap_hash = loopout_swaps.swap_hash
@ -304,8 +285,6 @@ type GetLoopOutSwapsRow struct {
PrepayInvoice string
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
SwapHash_3 []byte
SenderScriptPubkey []byte
ReceiverScriptPubkey []byte
@ -346,8 +325,6 @@ func (q *Queries) GetLoopOutSwaps(ctx context.Context) ([]GetLoopOutSwapsRow, er
&i.PrepayInvoice,
&i.MaxPrepayRoutingFee,
&i.PublicationDeadline,
&i.SingleSweep,
&i.PaymentTimeout,
&i.SwapHash_3,
&i.SenderScriptPubkey,
&i.ReceiverScriptPubkey,
@ -370,7 +347,7 @@ func (q *Queries) GetLoopOutSwaps(ctx context.Context) ([]GetLoopOutSwapsRow, er
}
const getSwapUpdates = `-- name: GetSwapUpdates :many
SELECT
SELECT
id, swap_hash, update_timestamp, update_state, htlc_txhash, server_cost, onchain_cost, offchain_cost
FROM
swap_updates
@ -488,11 +465,9 @@ INSERT INTO loopout_swaps (
outgoing_chan_set,
prepay_invoice,
max_prepay_routing_fee,
publication_deadline,
single_sweep,
payment_timeout
publication_deadline
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
)
`
@ -507,8 +482,6 @@ type InsertLoopOutParams struct {
PrepayInvoice string
MaxPrepayRoutingFee int64
PublicationDeadline time.Time
SingleSweep bool
PaymentTimeout int32
}
func (q *Queries) InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) error {
@ -523,8 +496,6 @@ func (q *Queries) InsertLoopOut(ctx context.Context, arg InsertLoopOutParams) er
arg.PrepayInvoice,
arg.MaxPrepayRoutingFee,
arg.PublicationDeadline,
arg.SingleSweep,
arg.PaymentTimeout,
)
return err
}
@ -611,29 +582,3 @@ func (q *Queries) InsertSwapUpdate(ctx context.Context, arg InsertSwapUpdatePara
)
return err
}
const overrideSwapCosts = `-- name: OverrideSwapCosts :exec
UPDATE swap_updates
SET
server_cost = $2,
onchain_cost = $3,
offchain_cost = $4
WHERE id = $1
`
type OverrideSwapCostsParams struct {
ID int32
ServerCost int64
OnchainCost int64
OffchainCost int64
}
func (q *Queries) OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error {
_, err := q.db.ExecContext(ctx, overrideSwapCosts,
arg.ID,
arg.ServerCost,
arg.OnchainCost,
arg.OffchainCost,
)
return err
}

@ -3,6 +3,7 @@ package loopdb
import (
"context"
"database/sql"
"errors"
"fmt"
"net/url"
"path/filepath"
@ -14,7 +15,7 @@ import (
"github.com/btcsuite/btcd/chaincfg"
sqlite_migrate "github.com/golang-migrate/migrate/v4/database/sqlite"
"github.com/lightninglabs/loop/loopdb/sqlc"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/require"
_ "modernc.org/sqlite" // Register relevant drivers.
)
@ -67,21 +68,6 @@ func NewSqliteStore(cfg *SqliteConfig, network *chaincfg.Params) (*SqliteSwapSto
name: "busy_timeout",
value: "5000",
},
{
// With the WAL mode, this ensures that we also do an
// extra WAL sync after each transaction. The normal
// sync mode skips this and gives better performance,
// but risks durability.
name: "synchronous",
value: "full",
},
{
// This is used to ensure proper durability for users
// running on Mac OS. It uses the correct fsync system
// call to ensure items are fully flushed to disk.
name: "fullfsync",
value: "true",
},
}
sqliteOptions := make(url.Values)
for _, option := range pragmaOptions {
@ -227,7 +213,7 @@ func (db *BaseDB) ExecTx(ctx context.Context, txOptions TxOptions,
func (b *BaseDB) FixFaultyTimestamps(ctx context.Context) error {
// Manually fetch all the loop out swaps.
rows, err := b.DB.QueryContext(
ctx, "SELECT swap_hash, swap_invoice, publication_deadline FROM loopout_swaps",
ctx, "SELECT swap_hash, publication_deadline FROM loopout_swaps",
)
if err != nil {
return err
@ -240,7 +226,6 @@ func (b *BaseDB) FixFaultyTimestamps(ctx context.Context) error {
// the sqlite driver will fail on faulty timestamps.
type LoopOutRow struct {
Hash []byte `json:"swap_hash"`
SwapInvoice string `json:"swap_invoice"`
PublicationDeadline string `json:"publication_deadline"`
}
@ -249,7 +234,7 @@ func (b *BaseDB) FixFaultyTimestamps(ctx context.Context) error {
for rows.Next() {
var swap LoopOutRow
err := rows.Scan(
&swap.Hash, &swap.SwapInvoice, &swap.PublicationDeadline,
&swap.Hash, &swap.PublicationDeadline,
)
if err != nil {
return err
@ -279,15 +264,14 @@ func (b *BaseDB) FixFaultyTimestamps(ctx context.Context) error {
// Skip if the year is not in the future.
thisYear := time.Now().Year()
if year > 2020 && year <= thisYear {
if year <= thisYear {
continue
}
payReq, err := zpay32.Decode(swap.SwapInvoice, b.network)
fixedTime, err := fixTimeStamp(swap.PublicationDeadline)
if err != nil {
return err
}
fixedTime := payReq.Timestamp.Add(time.Minute * 30)
// Update the faulty time to a valid time.
_, err = tx.ExecContext(
@ -310,7 +294,7 @@ func (b *BaseDB) FixFaultyTimestamps(ctx context.Context) error {
}
// TxOptions represents a set of options one can use to control what type of
// database transaction is created. Transaction can whether be read or write.
// database transaction is created. Transaction can wither be read or write.
type TxOptions interface {
// ReadOnly returns true if the transaction should be read only.
ReadOnly() bool
@ -338,6 +322,72 @@ func (r *SqliteTxOptions) ReadOnly() bool {
return r.readOnly
}
// fixTimeStamp tries to parse a timestamp string with both the
// parseSqliteTimeStamp and parsePostgresTimeStamp functions.
// If both fail, it returns an error.
func fixTimeStamp(dateTimeStr string) (time.Time, error) {
year, err := getTimeStampYear(dateTimeStr)
if err != nil {
return time.Time{}, err
}
// If the year is in the future. It was a faulty timestamp.
thisYear := time.Now().Year()
if year > thisYear {
dateTimeStr = strings.Replace(
dateTimeStr,
fmt.Sprintf("%d", year),
fmt.Sprintf("%d", thisYear),
1,
)
}
parsedTime, err := parseLayouts(defaultLayouts(), dateTimeStr)
if err != nil {
return time.Time{}, fmt.Errorf("unable to parse timestamp %v: %v",
dateTimeStr, err)
}
return parsedTime.UTC(), nil
}
// parseLayouts parses time based on a list of provided layouts.
// If layouts is empty list or nil, the error with unknown layout will be returned.
func parseLayouts(layouts []string, dateTime string) (time.Time, error) {
for _, layout := range layouts {
parsedTime, err := time.Parse(layout, dateTime)
if err == nil {
return parsedTime, nil
}
}
return time.Time{}, errors.New("unknown layout")
}
// defaultLayouts returns a default list of ALL supported layouts.
// This function returns new copy of a slice.
func defaultLayouts() []string {
return []string{
"2006-01-02 15:04:05.99999 -0700 MST", // Custom sqlite layout.
time.RFC3339Nano,
time.RFC3339,
time.RFC1123Z,
time.RFC1123,
time.RFC850,
time.RFC822Z,
time.RFC822,
time.Layout,
time.RubyDate,
time.UnixDate,
time.ANSIC,
time.StampNano,
time.StampMicro,
time.StampMilli,
time.Stamp,
time.Kitchen,
}
}
// getTimeStampYear returns the year of a timestamp string.
func getTimeStampYear(dateTimeStr string) (int, error) {
parts := strings.Split(dateTimeStr, "-")

@ -137,9 +137,6 @@ var (
// errInvalidKey is returned when a serialized key is not the expected
// length.
errInvalidKey = fmt.Errorf("invalid serialized key")
// errUnimplemented is returned when a method is not implemented.
errUnimplemented = fmt.Errorf("unimplemented method")
)
const (
@ -644,7 +641,7 @@ func (s *boltSwapStore) updateLoop(bucketKey []byte, hash lntypes.Hash,
}
updatesBucket := swapBucket.Bucket(updatesBucketKey)
if updatesBucket == nil {
return errors.New("update bucket not found")
return errors.New("udpate bucket not found")
}
// Each update for this swap will get a new monotonically
@ -993,41 +990,19 @@ func (s *boltSwapStore) fetchLoopInSwap(rootBucket *bbolt.Bucket,
func (b *boltSwapStore) BatchCreateLoopOut(ctx context.Context,
swaps map[lntypes.Hash]*LoopOutContract) error {
return errUnimplemented
return errors.New("not implemented")
}
// BatchCreateLoopIn creates a batch of loop in swaps to the store.
func (b *boltSwapStore) BatchCreateLoopIn(ctx context.Context,
swaps map[lntypes.Hash]*LoopInContract) error {
return errUnimplemented
return errors.New("not implemented")
}
// BatchInsertUpdate inserts batch of swap updates to the store.
func (b *boltSwapStore) BatchInsertUpdate(ctx context.Context,
updateData map[lntypes.Hash][]BatchInsertUpdateData) error {
return errUnimplemented
}
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of loop out
// swaps.
func (b *boltSwapStore) BatchUpdateLoopOutSwapCosts(ctx context.Context,
costs map[lntypes.Hash]SwapCost) error {
return errUnimplemented
}
// HasMigration returns true if the migration with the given ID has been done.
func (b *boltSwapStore) HasMigration(ctx context.Context, migrationID string) (
bool, error) {
return false, errUnimplemented
}
// SetMigration marks the migration with the given ID as done.
func (b *boltSwapStore) SetMigration(ctx context.Context,
migrationID string) error {
return errUnimplemented
return errors.New("not implemented")
}

@ -1,386 +0,0 @@
package loopdb
import (
"context"
"errors"
"fmt"
"testing"
"time"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require"
)
// StoreMock implements a mock client swap store.
type StoreMock struct {
LoopOutSwaps map[lntypes.Hash]*LoopOutContract
LoopOutUpdates map[lntypes.Hash][]SwapStateData
loopOutStoreChan chan LoopOutContract
loopOutUpdateChan chan SwapStateData
LoopInSwaps map[lntypes.Hash]*LoopInContract
LoopInUpdates map[lntypes.Hash][]SwapStateData
loopInStoreChan chan LoopInContract
loopInUpdateChan chan SwapStateData
migrations map[string]struct{}
t *testing.T
}
// NewStoreMock instantiates a new mock store.
func NewStoreMock(t *testing.T) *StoreMock {
return &StoreMock{
loopOutStoreChan: make(chan LoopOutContract, 1),
loopOutUpdateChan: make(chan SwapStateData, 1),
LoopOutSwaps: make(map[lntypes.Hash]*LoopOutContract),
LoopOutUpdates: make(map[lntypes.Hash][]SwapStateData),
loopInStoreChan: make(chan LoopInContract, 1),
loopInUpdateChan: make(chan SwapStateData, 1),
LoopInSwaps: make(map[lntypes.Hash]*LoopInContract),
LoopInUpdates: make(map[lntypes.Hash][]SwapStateData),
migrations: make(map[string]struct{}),
t: t,
}
}
// FetchLoopOutSwaps returns all swaps currently in the store.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut, error) {
result := []*LoopOut{}
for hash, contract := range s.LoopOutSwaps {
updates := s.LoopOutUpdates[hash]
events := make([]*LoopEvent, len(updates))
for i, u := range updates {
events[i] = &LoopEvent{
SwapStateData: u,
}
}
swap := &LoopOut{
Loop: Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
result = append(result, swap)
}
return result, nil
}
// FetchLoopOutSwaps returns all swaps currently in the store.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) FetchLoopOutSwap(ctx context.Context,
hash lntypes.Hash) (*LoopOut, error) {
contract, ok := s.LoopOutSwaps[hash]
if !ok {
return nil, errors.New("swap not found")
}
updates := s.LoopOutUpdates[hash]
events := make([]*LoopEvent, len(updates))
for i, u := range updates {
events[i] = &LoopEvent{
SwapStateData: u,
}
}
swap := &LoopOut{
Loop: Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
return swap, nil
}
// CreateLoopOut adds an initiated swap to the store.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) CreateLoopOut(ctx context.Context, hash lntypes.Hash,
swap *LoopOutContract) error {
_, ok := s.LoopOutSwaps[hash]
if ok {
return errors.New("swap already exists")
}
s.LoopOutSwaps[hash] = swap
s.LoopOutUpdates[hash] = []SwapStateData{}
s.loopOutStoreChan <- *swap
return nil
}
// FetchLoopInSwaps returns all in swaps currently in the store.
func (s *StoreMock) FetchLoopInSwaps(ctx context.Context) ([]*LoopIn,
error) {
result := []*LoopIn{}
for hash, contract := range s.LoopInSwaps {
updates := s.LoopInUpdates[hash]
events := make([]*LoopEvent, len(updates))
for i, u := range updates {
events[i] = &LoopEvent{
SwapStateData: u,
}
}
swap := &LoopIn{
Loop: Loop{
Hash: hash,
Events: events,
},
Contract: contract,
}
result = append(result, swap)
}
return result, nil
}
// CreateLoopIn adds an initiated loop in swap to the store.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) CreateLoopIn(ctx context.Context, hash lntypes.Hash,
swap *LoopInContract) error {
_, ok := s.LoopInSwaps[hash]
if ok {
return errors.New("swap already exists")
}
s.LoopInSwaps[hash] = swap
s.LoopInUpdates[hash] = []SwapStateData{}
s.loopInStoreChan <- *swap
return nil
}
// UpdateLoopOut stores a new event for a target loop out swap. This appends to
// the event log for a particular swap as it goes through the various stages in
// its lifetime.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) UpdateLoopOut(ctx context.Context, hash lntypes.Hash,
time time.Time, state SwapStateData) error {
updates, ok := s.LoopOutUpdates[hash]
if !ok {
return errors.New("swap does not exists")
}
updates = append(updates, state)
s.LoopOutUpdates[hash] = updates
s.loopOutUpdateChan <- state
return nil
}
// UpdateLoopIn stores a new event for a target loop in swap. This appends to
// the event log for a particular swap as it goes through the various stages in
// its lifetime.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) UpdateLoopIn(ctx context.Context, hash lntypes.Hash,
time time.Time, state SwapStateData) error {
updates, ok := s.LoopInUpdates[hash]
if !ok {
return errors.New("swap does not exists")
}
updates = append(updates, state)
s.LoopInUpdates[hash] = updates
s.loopInUpdateChan <- state
return nil
}
// PutLiquidityParams writes the serialized `manager.Parameters` bytes into the
// bucket.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) PutLiquidityParams(ctx context.Context,
params []byte) error {
return nil
}
// FetchLiquidityParams reads the serialized `manager.Parameters` bytes from
// the bucket.
//
// NOTE: Part of the SwapStore interface.
func (s *StoreMock) FetchLiquidityParams(ctx context.Context) ([]byte, error) {
return nil, nil
}
// Close closes the store.
func (s *StoreMock) Close() error {
return nil
}
// isDone asserts that the store mock has no pending operations.
func (s *StoreMock) IsDone() error {
select {
case <-s.loopOutStoreChan:
return errors.New("storeChan not empty")
default:
}
select {
case <-s.loopOutUpdateChan:
return errors.New("updateChan not empty")
default:
}
return nil
}
// AssertLoopOutStored asserts that a swap is stored.
func (s *StoreMock) AssertLoopOutStored() {
s.t.Helper()
select {
case <-s.loopOutStoreChan:
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be stored")
}
}
// AssertLoopOutState asserts that a specified state transition is persisted to
// disk.
func (s *StoreMock) AssertLoopOutState(expectedState SwapState) {
s.t.Helper()
select {
case state := <-s.loopOutUpdateChan:
require.Equal(s.t, expectedState, state.State)
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap state to be stored")
}
}
// AssertLoopInStored asserts that a loop-in swap is stored.
func (s *StoreMock) AssertLoopInStored() {
s.t.Helper()
select {
case <-s.loopInStoreChan:
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be stored")
}
}
// assertLoopInState asserts that a specified state transition is persisted to
// disk.
func (s *StoreMock) AssertLoopInState(
expectedState SwapState) SwapStateData {
s.t.Helper()
state := <-s.loopInUpdateChan
require.Equal(s.t, expectedState, state.State)
return state
}
// AssertStorePreimageReveal asserts that a swap is marked as preimage revealed.
func (s *StoreMock) AssertStorePreimageReveal() {
s.t.Helper()
select {
case state := <-s.loopOutUpdateChan:
require.Equal(s.t, StatePreimageRevealed, state.State)
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be marked as preimage revealed")
}
}
// AssertStoreFinished asserts that a swap is marked as finished.
func (s *StoreMock) AssertStoreFinished(expectedResult SwapState) {
s.t.Helper()
select {
case state := <-s.loopOutUpdateChan:
require.Equal(s.t, expectedResult, state.State)
case <-time.After(test.Timeout):
s.t.Fatalf("expected swap to be finished")
}
}
// BatchCreateLoopOut creates many loop out swaps in a batch.
func (b *StoreMock) BatchCreateLoopOut(ctx context.Context,
swaps map[lntypes.Hash]*LoopOutContract) error {
return errors.New("not implemented")
}
// BatchCreateLoopIn creates many loop in swaps in a batch.
func (b *StoreMock) BatchCreateLoopIn(ctx context.Context,
swaps map[lntypes.Hash]*LoopInContract) error {
return errors.New("not implemented")
}
// BatchInsertUpdate inserts many updates for a swap in a batch.
func (b *StoreMock) BatchInsertUpdate(ctx context.Context,
updateData map[lntypes.Hash][]BatchInsertUpdateData) error {
return errors.New("not implemented")
}
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of loop out
// swaps.
func (s *StoreMock) BatchUpdateLoopOutSwapCosts(ctx context.Context,
costs map[lntypes.Hash]SwapCost) error {
for hash, cost := range costs {
if _, ok := s.LoopOutUpdates[hash]; !ok {
return fmt.Errorf("swap has no updates: %v", hash)
}
updates, ok := s.LoopOutUpdates[hash]
if !ok {
return fmt.Errorf("swap has no updates: %v", hash)
}
updates[len(updates)-1].Cost = cost
}
return nil
}
// HasMigration returns true if the migration with the given ID has been done.
func (s *StoreMock) HasMigration(ctx context.Context, migrationID string) (
bool, error) {
_, ok := s.migrations[migrationID]
return ok, nil
}
// SetMigration marks the migration with the given ID as done.
func (s *StoreMock) SetMigration(ctx context.Context,
migrationID string) error {
if _, ok := s.migrations[migrationID]; ok {
return errors.New("migration already done")
}
s.migrations[migrationID] = struct{}{}
return nil
}

@ -64,19 +64,6 @@ const (
// StateFailIncorrectHtlcAmt indicates that the amount of an externally
// published loop in htlc didn't match the swap amount.
StateFailIncorrectHtlcAmt SwapState = 10
// StateFailAbandoned indicates that a swap has been abandoned. Its
// execution has been canceled. It won't further be processed.
StateFailAbandoned SwapState = 11
// StateFailInsufficientConfirmedBalance indicates that the swap wasn't
// published due to insufficient confirmed balance.
StateFailInsufficientConfirmedBalance SwapState = 12
// StateFailIncorrectHtlcAmtSwept indicates that the amount of an
// externally published loop in htlc that didn't match the swap amount
// has been swept back to the user after the htlc timeout period.
StateFailIncorrectHtlcAmtSwept SwapState = 13
)
// SwapStateType defines the types of swap states that exist. Every swap state
@ -97,7 +84,10 @@ const (
// Type returns the type of the SwapState it is called on.
func (s SwapState) Type() SwapStateType {
if s.IsPending() {
if s == StateInitiated || s == StateHtlcPublished ||
s == StatePreimageRevealed || s == StateFailTemporary ||
s == StateInvoiceSettled {
return StateTypePending
}
@ -108,18 +98,6 @@ func (s SwapState) Type() SwapStateType {
return StateTypeFail
}
// IsPending returns true if the swap is in a pending state.
func (s SwapState) IsPending() bool {
return s == StateInitiated || s == StateHtlcPublished ||
s == StatePreimageRevealed || s == StateFailTemporary ||
s == StateInvoiceSettled || s == StateFailIncorrectHtlcAmt
}
// IsFinal returns true if the swap is in a final state.
func (s SwapState) IsFinal() bool {
return !s.IsPending()
}
// String returns a string representation of the swap's state.
func (s SwapState) String() string {
switch s {
@ -156,15 +134,6 @@ func (s SwapState) String() string {
case StateFailIncorrectHtlcAmt:
return "IncorrectHtlcAmt"
case StateFailAbandoned:
return "FailAbandoned"
case StateFailInsufficientConfirmedBalance:
return "InsufficientConfirmedBalance"
case StateFailIncorrectHtlcAmtSwept:
return "StateFailIncorrectHtlcAmtSwept"
default:
return "Unknown"
}

@ -18,7 +18,6 @@ import (
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/keychain"
@ -49,10 +48,6 @@ var (
// TimeoutTxConfTarget defines the confirmation target for the loop in
// timeout tx.
TimeoutTxConfTarget = int32(2)
// ErrSwapFinalized is returned when a to be executed swap is already in
// a final state.
ErrSwapFinalized = errors.New("swap is in a final state")
)
// loopInSwap contains all the in-memory state related to a pending loop in
@ -75,8 +70,6 @@ type loopInSwap struct {
timeoutAddr btcutil.Address
abandonChan chan struct{}
wg sync.WaitGroup
}
@ -111,8 +104,8 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
// Because the Private flag is set, we'll generate our own set
// of hop hints.
request.RouteHints, err = SelectHopHints(
globalCtx, cfg.lnd.Client, request.Amount,
DefaultMaxHopHints, includeNodes,
globalCtx, cfg.lnd, request.Amount, DefaultMaxHopHints,
includeNodes,
)
if err != nil {
return nil, err
@ -315,8 +308,6 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
swap.log.Infof("Server message: %v", swapResp.serverMessage)
}
swap.abandonChan = make(chan struct{}, 1)
return &loopInInitResult{
swap: swap,
serverMessage: swapResp.serverMessage,
@ -422,10 +413,6 @@ func resumeLoopInSwap(_ context.Context, cfg *swapConfig,
swap.cost = lastUpdate.Cost
}
// Upon restoring the swap we also need to assign a new abandon channel
// that the client can use to signal that the swap should be abandoned.
swap.abandonChan = make(chan struct{}, 1)
return swap, nil
}
@ -443,7 +430,7 @@ func validateLoopInContract(height int32, response *newLoopInResponse) error {
// initHtlcs creates and updates the native and nested segwit htlcs of the
// loopInSwap.
func (s *loopInSwap) initHtlcs() error {
htlc, err := utils.GetHtlc(
htlc, err := GetHtlc(
s.hash, &s.SwapContract, s.swapKit.lnd.ChainParams,
)
if err != nil {
@ -531,18 +518,6 @@ func (s *loopInSwap) execute(mainCtx context.Context,
// error occurs.
err = s.executeSwap(mainCtx)
// If there are insufficient confirmed funds to publish the swap, we
// finalize its state so a new swap will be published if funds become
// available.
if errors.Is(err, ErrInsufficientBalance) {
return err
}
// Stop the execution if the swap has been abandoned.
if err != nil && s.state == loopdb.StateFailAbandoned {
return err
}
// Sanity check. If there is no error, the swap must be in a final
// state.
if err == nil && s.state.Type() == loopdb.StateTypePending {
@ -578,11 +553,6 @@ func (s *loopInSwap) execute(mainCtx context.Context,
func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
var err error
// If the swap is already in a final state, we can return immediately.
if s.state.IsFinal() {
return ErrSwapFinalized
}
// For loop in, the client takes the first step by publishing the
// on-chain htlc. Only do this if we haven't already done so in a
// previous run.
@ -623,18 +593,10 @@ func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
}
// Verify that the confirmed (external) htlc value matches the swap
// amount. If the amounts mismatch we update the swap state to indicate
// this, but end processing the swap. Instead, we continue to wait for
// the htlc to expire and publish a timeout tx to reclaim the funds. We
// skip this part if the swap was recovered from this state.
if s.state != loopdb.StateFailIncorrectHtlcAmt &&
htlcValue != s.LoopInContract.AmountRequested {
// amount. Otherwise, fail the swap immediately.
if htlcValue != s.LoopInContract.AmountRequested {
s.setState(loopdb.StateFailIncorrectHtlcAmt)
err = s.persistAndAnnounceState(globalCtx)
if err != nil {
log.Errorf("Error persisting state: %v", err)
}
return s.persistAndAnnounceState(globalCtx)
}
// The server is expected to see the htlc on-chain and know that it can
@ -726,11 +688,6 @@ func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) (
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
// If the client requested the swap to be abandoned, we override
// the status in the database.
case <-s.abandonChan:
return nil, s.setStateAbandoned(ctx)
// Cancel.
case <-globalCtx.Done():
return nil, globalCtx.Err()
@ -797,14 +754,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
}}, feeRate, labels.LoopInHtlcLabel(swap.ShortHash(&s.hash)),
)
if err != nil {
s.log.Errorf("send outputs: %v", err)
s.setState(loopdb.StateFailInsufficientConfirmedBalance)
// If we cannot send out this update, there is nothing we can
// do.
_ = s.persistAndAnnounceState(ctx)
return false, ErrInsufficientBalance
return false, fmt.Errorf("send outputs: %v", err)
}
txHash := tx.TxHash()
@ -837,7 +787,7 @@ func getTxFee(tx *wire.MsgTx, fee chainfee.SatPerKVByte) btcutil.Amount {
btcTx := btcutil.NewTx(tx)
vsize := mempool.GetTxVirtualSize(btcTx)
return fee.FeeForVSize(lntypes.VByte(vsize))
return fee.FeeForVSize(vsize)
}
// waitForSwapComplete waits until a spending tx of the htlc gets confirmed and
@ -890,11 +840,6 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
htlcKeyRevealed := false
for !htlcSpend || !invoiceFinalized {
select {
// If the client requested the swap to be abandoned, we override
// the status in the database.
case <-s.abandonChan:
return s.setStateAbandoned(ctx)
// Spend notification error.
case err := <-spendErr:
return err
@ -919,7 +864,9 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
s.log.Infof("Htlc spend by tx: %v",
spendDetails.SpenderTxHash)
err := s.processHtlcSpend(ctx, spendDetails, sweepFee)
err := s.processHtlcSpend(
ctx, spendDetails, htlcValue, sweepFee,
)
if err != nil {
return err
}
@ -957,6 +904,8 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
switch update.State {
// Swap invoice was paid, so update server cost balance.
case invpkg.ContractSettled:
s.cost.Server -= update.AmtPaid
// If invoice settlement and htlc spend happen
// in the expected order, move the swap to an
// intermediate state that indicates that the
@ -973,8 +922,6 @@ func (s *loopInSwap) waitForSwapComplete(ctx context.Context,
invoiceFinalized = true
htlcKeyRevealed = s.tryPushHtlcKey(ctx)
s.cost.Server = s.AmountRequested -
update.AmtPaid
// Canceled invoice has no effect on server cost
// balance.
@ -1021,7 +968,8 @@ func (s *loopInSwap) tryPushHtlcKey(ctx context.Context) bool {
}
func (s *loopInSwap) processHtlcSpend(ctx context.Context,
spend *chainntnfs.SpendDetail, sweepFee btcutil.Amount) error {
spend *chainntnfs.SpendDetail, htlcValue,
sweepFee btcutil.Amount) error {
// Determine the htlc input of the spending tx and inspect the witness
// to find out whether a success or a timeout tx spent the htlc.
@ -1029,20 +977,15 @@ func (s *loopInSwap) processHtlcSpend(ctx context.Context,
if s.htlc.IsSuccessWitness(htlcInput.Witness) {
s.setState(loopdb.StateSuccess)
// Server swept the htlc. The htlc value can be added to the
// server cost balance.
s.cost.Server += htlcValue
} else {
// We needed another on chain tx to sweep the timeout clause,
// which we now include in our costs.
s.cost.Onchain += sweepFee
// If the swap is in state StateFailIncorrectHtlcAmt we know
// that the deposited htlc amount wasn't equal to the contract
// amount. We can finalize the swap by setting an appropriate
// state.
if s.state == loopdb.StateFailIncorrectHtlcAmt {
s.setState(loopdb.StateFailIncorrectHtlcAmtSwept)
} else {
s.setState(loopdb.StateFailTimeout)
}
s.setState(loopdb.StateFailTimeout)
// Now that the timeout tx confirmed, we can safely cancel the
// swap invoice. We still need to query the final invoice state.
@ -1119,31 +1062,6 @@ func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
return fee, nil
}
// setStateAbandoned stores the abandoned state and announces it. It also
// cancels the swap invoice so the server can't settle it.
func (s *loopInSwap) setStateAbandoned(ctx context.Context) error {
s.log.Infof("Abandoning swap %v...", s.hash)
if !s.state.IsPending() {
return fmt.Errorf("cannot abandon swap in state %v", s.state)
}
s.setState(loopdb.StateFailAbandoned)
err := s.persistAndAnnounceState(ctx)
if err != nil {
return err
}
// If the invoice is already settled or canceled, this is a nop.
_ = s.lnd.Invoices.CancelInvoice(ctx, s.hash)
return fmt.Errorf("swap hash "+
"abandoned by client, "+
"swap ID: %v, %v",
s.hash, err)
}
// persistAndAnnounceState updates the swap state on disk and sends out an
// update notification.
func (s *loopInSwap) persistAndAnnounceState(ctx context.Context) error {

@ -9,7 +9,6 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test"
"github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs"
invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/routing/route"
@ -62,7 +61,7 @@ func testLoopInSuccess(t *testing.T) {
inSwap := initResult.swap
ctx.store.AssertLoopInStored()
ctx.store.assertLoopInStored()
errChan := make(chan error)
go func() {
@ -83,7 +82,7 @@ func testLoopInSuccess(t *testing.T) {
require.Nil(t, swapInfo.OutgoingChanSet)
ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published.
htlcTx := <-ctx.lnd.SendOutputsChannel
@ -96,7 +95,7 @@ func testLoopInSuccess(t *testing.T) {
// Expect the same state to be written again with the htlc tx hash
// and on chain fee.
state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
require.NotNil(t, state.HtlcTxHash)
require.Equal(t, cost, state.Cost)
@ -120,7 +119,7 @@ func testLoopInSuccess(t *testing.T) {
// Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled)
// Server spends htlc.
successTx := wire.MsgTx{}
@ -139,7 +138,7 @@ func testLoopInSuccess(t *testing.T) {
}
ctx.assertState(loopdb.StateSuccess)
ctx.store.AssertLoopInState(loopdb.StateSuccess)
ctx.store.assertLoopInState(loopdb.StateSuccess)
require.NoError(t, <-errChan)
}
@ -214,7 +213,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
require.NoError(t, err)
inSwap := initResult.swap
ctx.store.AssertLoopInStored()
ctx.store.assertLoopInStored()
errChan := make(chan error)
go func() {
@ -228,7 +227,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
ctx.assertState(loopdb.StateInitiated)
ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
var (
htlcTx wire.MsgTx
@ -247,7 +246,7 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
// Expect the same state to be written again with the htlc tx
// hash and cost.
state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
require.NotNil(t, state.HtlcTxHash)
require.Equal(t, cost, state.Cost)
} else {
@ -277,19 +276,14 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
Tx: &htlcTx,
}
isInvalidAmt := externalValue != 0 && externalValue != int64(req.Amount)
handleHtlcExpiry(
t, ctx, inSwap, htlcTx, cost, errChan, isInvalidAmt,
)
}
func handleHtlcExpiry(t *testing.T, ctx *loopInTestContext, inSwap *loopInSwap,
htlcTx wire.MsgTx, cost loopdb.SwapCost, errChan chan error,
isInvalidAmount bool) {
if isInvalidAmount {
ctx.store.AssertLoopInState(loopdb.StateFailIncorrectHtlcAmt)
// Assert that the swap is failed in case of an invalid amount.
invalidAmt := externalValue != 0 && externalValue != int64(req.Amount)
if invalidAmt {
ctx.assertState(loopdb.StateFailIncorrectHtlcAmt)
ctx.store.assertLoopInState(loopdb.StateFailIncorrectHtlcAmt)
require.NoError(t, <-errChan)
return
}
// Client starts listening for spend of htlc.
@ -334,16 +328,8 @@ func handleHtlcExpiry(t *testing.T, ctx *loopInTestContext, inSwap *loopInSwap,
// Signal that the invoice was canceled.
ctx.updateInvoiceState(0, invpkg.ContractCanceled)
var state loopdb.SwapStateData
if isInvalidAmount {
state = ctx.store.AssertLoopInState(
loopdb.StateFailIncorrectHtlcAmtSwept,
)
ctx.assertState(loopdb.StateFailIncorrectHtlcAmtSwept)
} else {
ctx.assertState(loopdb.StateFailTimeout)
state = ctx.store.AssertLoopInState(loopdb.StateFailTimeout)
}
ctx.assertState(loopdb.StateFailTimeout)
state := ctx.store.assertLoopInState(loopdb.StateFailTimeout)
require.Equal(t, cost, state.Cost)
require.NoError(t, <-errChan)
@ -463,7 +449,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
pendSwap.Loop.Events[0].Cost = cost
}
htlc, err := utils.GetHtlc(
htlc, err := GetHtlc(
testPreimage.Hash(), &contract.SwapContract,
cfg.lnd.ChainParams,
)
@ -517,7 +503,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
}
ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published.
htlcTx = <-ctx.lnd.SendOutputsChannel
@ -529,7 +515,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
// Expect the same state to be written again with the htlc tx
// hash.
state := ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
state := ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
require.NotNil(t, state.HtlcTxHash)
} else {
ctx.assertState(loopdb.StateHtlcPublished)
@ -561,7 +547,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
// Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled)
ctx.store.assertLoopInState(loopdb.StateInvoiceSettled)
// Server spends htlc.
successTx := wire.MsgTx{}
@ -580,7 +566,7 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
}
ctx.assertState(loopdb.StateSuccess)
finalState := ctx.store.AssertLoopInState(loopdb.StateSuccess)
finalState := ctx.store.assertLoopInState(loopdb.StateSuccess)
// We expect our server fee to reflect as the difference between htlc
// value and invoice amount paid. We use our original on-chain cost, set
@ -588,205 +574,3 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
cost.Server = btcutil.Amount(htlcTx.TxOut[0].Value) - amtPaid
require.Equal(t, cost, finalState.Cost)
}
// TestAbandonPublishedHtlcState advances a loop-in swap to StateHtlcPublished,
// then abandons it and ensures that executing the same swap would not progress.
func TestAbandonPublishedHtlcState(t *testing.T) {
defer test.Guard(t)()
ctx := newLoopInTestContext(t)
height := int32(600)
cfg, err, inSwap := startNewLoopIn(t, ctx, height)
require.NoError(t, err)
advanceToPublishedHtlc(t, ctx)
// The client requests to abandon the published htlc state.
inSwap.abandonChan <- struct{}{}
// Ensure that the swap is now in the StateFailAbandoned state.
ctx.assertState(loopdb.StateFailAbandoned)
// Ensure that the swap is also in the StateFailAbandoned state in the
// database.
ctx.store.AssertLoopInState(loopdb.StateFailAbandoned)
// Ensure that the swap was abandoned and the execution stopped.
err = <-ctx.errChan
require.Error(t, err)
require.Contains(t, err.Error(), "swap hash abandoned by client")
// We re-instantiate the swap and ensure that it does not progress.
pendSwap := &loopdb.LoopIn{
Contract: &inSwap.LoopInContract,
Loop: loopdb.Loop{
Events: []*loopdb.LoopEvent{
{
SwapStateData: loopdb.SwapStateData{
State: inSwap.state,
},
},
},
Hash: testPreimage.Hash(),
},
}
resumedSwap, err := resumeLoopInSwap(
context.Background(), cfg, pendSwap,
)
require.NoError(t, err)
// Execute the abandoned swap.
go func() {
err := resumedSwap.execute(
context.Background(), ctx.cfg, height,
)
if err != nil {
log.Error(err)
}
ctx.errChan <- err
}()
// Ensure that the swap is still in the StateFailAbandoned state.
swapInfo := <-ctx.statusChan
require.Equal(t, loopdb.StateFailAbandoned, swapInfo.State)
// Ensure that the execution flagged the abandoned swap as finalized.
err = <-ctx.errChan
require.Error(t, err)
require.Equal(t, ErrSwapFinalized, err)
}
// TestAbandonSettledInvoiceState advances a loop-in swap to
// StateInvoiceSettled, then abandons it and ensures that executing the same
// swap would not progress.
func TestAbandonSettledInvoiceState(t *testing.T) {
defer test.Guard(t)()
ctx := newLoopInTestContext(t)
height := int32(600)
cfg, err, inSwap := startNewLoopIn(t, ctx, height)
require.NoError(t, err)
advanceToPublishedHtlc(t, ctx)
// Client starts listening for swap invoice updates.
ctx.assertSubscribeInvoice(ctx.server.swapHash)
// Server has already paid invoice before spending the htlc. Signal
// settled.
ctx.updateInvoiceState(49000, invpkg.ContractSettled)
// Swap is expected to move to the state InvoiceSettled
ctx.assertState(loopdb.StateInvoiceSettled)
ctx.store.AssertLoopInState(loopdb.StateInvoiceSettled)
// The client requests to abandon the published htlc state.
inSwap.abandonChan <- struct{}{}
// Ensure that the swap is now in the StateFailAbandoned state.
ctx.assertState(loopdb.StateFailAbandoned)
// Ensure that the swap is also in the StateFailAbandoned state in the
// database.
ctx.store.AssertLoopInState(loopdb.StateFailAbandoned)
// Ensure that the swap was abandoned and the execution stopped.
err = <-ctx.errChan
require.Error(t, err)
require.Contains(t, err.Error(), "swap hash abandoned by client")
// We re-instantiate the swap and ensure that it does not progress.
pendSwap := &loopdb.LoopIn{
Contract: &inSwap.LoopInContract,
Loop: loopdb.Loop{
Events: []*loopdb.LoopEvent{
{
SwapStateData: loopdb.SwapStateData{
State: inSwap.state,
},
},
},
Hash: testPreimage.Hash(),
},
}
resumedSwap, err := resumeLoopInSwap(context.Background(), cfg, pendSwap)
require.NoError(t, err)
// Execute the abandoned swap.
go func() {
err := resumedSwap.execute(
context.Background(), ctx.cfg, height,
)
if err != nil {
log.Error(err)
}
ctx.errChan <- err
}()
// Ensure that the swap is still in the StateFailAbandoned state.
swapInfo := <-ctx.statusChan
require.Equal(t, loopdb.StateFailAbandoned, swapInfo.State)
// Ensure that the execution flagged the abandoned swap as finalized.
err = <-ctx.errChan
require.Error(t, err)
require.Equal(t, ErrSwapFinalized, err)
}
func advanceToPublishedHtlc(t *testing.T, ctx *loopInTestContext) SwapInfo {
swapInfo := <-ctx.statusChan
require.Equal(t, loopdb.StateInitiated, swapInfo.State)
ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published.
htlcTx := <-ctx.lnd.SendOutputsChannel
// Expect the same state to be written again with the htlc tx hash
// and on chain fee.
ctx.store.AssertLoopInState(loopdb.StateHtlcPublished)
// Expect register for htlc conf (only one, since the htlc is p2tr).
<-ctx.lnd.RegisterConfChannel
// Confirm htlc.
ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: &htlcTx,
}
// Client starts listening for spend of htlc.
<-ctx.lnd.RegisterSpendChannel
return swapInfo
}
func startNewLoopIn(t *testing.T, ctx *loopInTestContext, height int32) (
*swapConfig, error, *loopInSwap) {
cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server)
req := &testLoopInRequest
initResult, err := newLoopInSwap(
context.Background(), cfg,
height, req,
)
require.NoError(t, err)
inSwap := initResult.swap
ctx.store.AssertLoopInStored()
go func() {
err := inSwap.execute(context.Background(), ctx.cfg, height)
if err != nil {
log.Error(err)
}
ctx.errChan <- err
}()
return cfg, err, inSwap
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save