Compare commits

..

No commits in common. 'master' and 'v0.26.1-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:
########################

@ -76,9 +76,6 @@ linters:
- golint
- maligned
- scopelint
- varcheck
- structcheck
- deadcode
# New linters that need a code adjustment first.
- wrapcheck
@ -110,10 +107,6 @@ linters:
- stylecheck
- thelper
- revive
- tagalign
- depguard
- nosnakecase
- interfacebloat
# Additions compared to LND
- exhaustruct
@ -129,10 +122,6 @@ issues:
linters:
- forbidigo
- unparam
- gosec
- path: _mock\.go
linters:
- gosec
# Allow fmt.Printf() in loopd
- path: cmd/loopd/*
@ -144,10 +133,5 @@ issues:
# Allow fmt.Printf() in loop
- path: cmd/loop/*
linters:
- forbidigo
# Allow fmt.Printf() in stateparser
- path: fsm/stateparser/*
linters:
- forbidigo

@ -0,0 +1,31 @@
language: go
cache:
directories:
- $GOCACHE
- $GOPATH/pkg/mod
- $GOPATH/src/github.com/btcsuite
- $GOPATH/src/github.com/golang
- $GOPATH/src/gopkg.in/alecthomas
# Remove Travis' default flag --depth=50 from the git clone command to make sure
# we have the whole git history, including the commit we lint against.
git:
depth: false
go:
- "1.19.2"
env:
global:
- GOCACHE=$HOME/.go-build
sudo: required
script:
- export GO111MODULE=on
- make lint unit build mod-check rpc-js-compile
- make tags=dev
after_script:
- echo "Uploading to termbin.com..." && find *.log | xargs -I{} sh -c "cat {} | nc termbin.com 9999 | xargs -r0 printf '{} uploaded to %s'"
- echo "Uploading to file.io..." && tar -zcvO *.log | curl -s -F 'file=@-;filename=logs.tar.gz' https://file.io | xargs -r0 printf 'logs.tar.gz uploaded to %s\n'

@ -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"
@ -138,7 +134,4 @@ sqlc-check: sqlc
@$(call print, "Verifying sql code generation.")
if test -n "$$(git status --porcelain '*.go')"; then echo "SQL models not properly generated!"; git status --porcelain '*.go'; exit 1; fi
fsm:
@$(call print, "Generating state machine docs")
./scripts/fsm-generate.sh;
.PHONY: fsm

@ -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)
@ -242,8 +204,6 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
swaps := make([]*SwapInfo, 0, len(loopInSwaps)+len(loopOutSwaps))
for _, swp := range loopOutSwaps {
swp := swp
swapInfo := &SwapInfo{
SwapType: swap.TypeOut,
SwapContract: swp.Contract.SwapContract,
@ -252,7 +212,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,
)
@ -275,8 +235,6 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
}
for _, swp := range loopInSwaps {
swp := swp
swapInfo := &SwapInfo{
SwapType: swap.TypeIn,
SwapContract: swp.Contract.SwapContract,
@ -285,7 +243,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,
)
@ -314,7 +272,9 @@ func (s *Client) FetchSwaps(ctx context.Context) ([]*SwapInfo, error) {
// restored from persistent storage and resumed. Subsequent updates will be
// sent through the passed in statusChan. The function can be terminated by
// cancelling the context.
func (s *Client) Run(ctx context.Context, statusChan chan<- SwapInfo) error {
func (s *Client) Run(ctx context.Context,
statusChan chan<- SwapInfo) error {
if !atomic.CompareAndSwapUint32(&s.started, 0, 1) {
return errors.New("swap client can only be started once")
}
@ -324,7 +284,7 @@ func (s *Client) Run(ctx context.Context, statusChan chan<- SwapInfo) error {
s.lndServices.NodeAlias, s.lndServices.NodePubkey,
lndclient.VersionString(s.lndServices.Version))
// Setup main context used for cancellation.
// Setup main context used for cancelation.
mainCtx, mainCancel := context.WithCancel(ctx)
defer mainCancel()
@ -347,7 +307,7 @@ func (s *Client) Run(ctx context.Context, statusChan chan<- SwapInfo) error {
s.resumeSwaps(mainCtx, pendingLoopOutSwaps, pendingLoopInSwaps)
// Signal that new requests can be accepted. Otherwise, the new
// Signal that new requests can be accepted. Otherwise the new
// swap could already have been added to the store and read in
// this goroutine as being a swap that needs to be resumed.
// Resulting in two goroutines executing the same swap.
@ -355,10 +315,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 +372,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 +514,7 @@ func (s *Client) getLoopOutSweepFee(ctx context.Context, confTarget int32) (
return 0, err
}
scriptVersion := utils.GetHtlcScriptVersion(
scriptVersion := GetHtlcScriptVersion(
loopdb.CurrentProtocolVersion(),
)
@ -622,10 +576,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 +633,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 +701,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 +751,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
}

@ -19,8 +19,9 @@ var (
confTargetFlag = cli.Uint64Flag{
Name: "conf_target",
Usage: "the target number of blocks the on-chain htlc " +
"broadcast by the swap client should confirm within",
Usage: "the target number of blocks the on-chain " +
"htlc broadcast by the swap client should " +
"confirm within",
}
labelFlag = cli.StringFlag{
@ -32,20 +33,19 @@ var (
}
routeHintsFlag = cli.StringSliceFlag{
Name: "route_hints",
Usage: "route hints that can each be individually used " +
"to assist in reaching the invoice's destination",
Usage: "Route hints that can each be individually used " +
"to assist in reaching the invoice's destination.",
}
privateFlag = cli.BoolFlag{
Name: "private",
Usage: "generates and passes routehints. Should be used if " +
"the connected node is only reachable via private " +
Usage: "Generates and passes routehints. Should be used " +
"if the connected node is only reachable via private " +
"channels",
}
forceFlag = cli.BoolFlag{
Name: "force, f",
Usage: "Assumes yes during confirmation. Using this option " +
"will result in an immediate swap",
Name: "force, f",
Usage: "Assumes yes during confirmation. Using this option will result in an immediate swap",
}
loopInCommand = cli.Command{
@ -66,11 +66,8 @@ var (
`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "amt",
Usage: "the amount in satoshis to loop in. " +
"To check for the minimum and " +
"maximum amounts to loop " +
"in please consult \"loop terms\"",
Name: "amt",
Usage: "the amount in satoshis to loop in",
},
cli.BoolFlag{
Name: "external",

@ -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 " +
@ -47,7 +44,7 @@ var loopOutCommand = cli.Command{
Usage: "the name of the account to generate a new " +
"address from. You can list the names of " +
"valid accounts in your backing lnd " +
"instance with \"lncli wallet accounts list\"",
"instance with \"lncli wallet accounts list\".",
Value: "",
},
cli.StringFlag{
@ -58,16 +55,14 @@ var loopOutCommand = cli.Command{
Value: "p2tr",
},
cli.Uint64Flag{
Name: "amt",
Usage: "the amount in satoshis to loop out. To check " +
"for the minimum and maximum amounts to loop " +
"out please consult \"loop terms\"",
Name: "amt",
Usage: "the amount in satoshis to loop out",
},
cli.Uint64Flag{
Name: "htlc_confs",
Usage: "the number of confirmations (in blocks) " +
"that we require for the htlc extended by " +
"the server before we reveal the preimage",
"the server before we reveal the preimage.",
Value: uint64(loopdb.DefaultLoopOutHtlcConfirmations),
},
cli.Uint64Flag{
@ -85,26 +80,17 @@ var loopOutCommand = cli.Command{
},
cli.BoolFlag{
Name: "fast",
Usage: "indicate you want to swap immediately, " +
Usage: "Indicate you want to swap immediately, " +
"paying potentially a higher fee. If not " +
"set the swap server might choose to wait up " +
"to 30 minutes before publishing the swap " +
"HTLC on-chain, to save on its chain fees. " +
"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",
"result in a lower swap fee.",
},
forceFlag,
labelFlag,
verboseFlag,
channelFlag,
},
Action: loopOut,
}
@ -244,29 +230,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 +246,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 {

@ -2,7 +2,6 @@ package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
@ -18,13 +17,14 @@ import (
"github.com/lightninglabs/loop/loopd"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/protobuf-hex-display/json"
"github.com/lightninglabs/protobuf-hex-display/jsonpb"
"github.com/lightninglabs/protobuf-hex-display/proto"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/urfave/cli"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/protobuf/proto"
"gopkg.in/macaroon.v2"
)
@ -111,13 +111,19 @@ func printJSON(resp interface{}) {
}
func printRespJSON(resp proto.Message) {
jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
jsonMarshaler := &jsonpb.Marshaler{
OrigName: true,
EmitDefaults: true,
Indent: " ",
}
jsonStr, err := jsonMarshaler.MarshalToString(resp)
if err != nil {
fmt.Println("unable to decode response: ", err)
return
}
fmt.Println(string(jsonBytes))
fmt.Println(jsonStr)
}
func fatal(err error) {
@ -147,8 +153,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)
}

@ -1,9 +1,9 @@
# Autoloop
The Loop client contains functionality to dispatch Loop Out swaps automatically,
The loop client contains functionality to dispatch loop out swaps automatically,
according to a set of rules configured for your node's channels, within a
budget of your choosing.
The Autoloop functionality is disabled by default, and can be enabled using the
The autoloop functionality is disabled by default, and can be enabled using the
following command:
```
loop setparams --autoloop=true
@ -12,18 +12,18 @@ loop setparams --autoloop=true
## Easy Autoloop
If you don't want to bother with setting up specific rules for each of your
channels and peers you can use easy Autoloop. This mode of Autoloop requires
channels and peers you can use easy autoloop. This mode of autoloop requires
for you to set only the overall channel balance that you don't wish to exceed
on your Lightning node. For example, if you want to keep your node's total
on your lightning node. For example, if you want to keep your node's total
channel balance below 1 million satoshis you can set the following
```
loop setparams --autoloop=true --easyautoloop=true --localbalancesat=1000000
```
This will automatically start dispatching Loop Outs whenever you exceed a total
This will automatically start dispatching loop-outs whenever you exceed total
channel balance of 1M sats. Keep in mind that on first time use this will use
the default budget parameters. If you wish to configure a custom budget you can
find more information in the [Budget](#budget) section.
find more info in the [Budget](#budget) section.
In addition, if you want to control the maximum amount of fees you're willing to
spend per swap, as a percentage of the total swap amount, you can use the
@ -31,24 +31,24 @@ spend per swap, as a percentage of the total swap amount, you can use the
## Liquidity Rules
At present, Autoloop can be configured to either acquire incoming liquidity
using Loop Out, or acquire outgoing liquidity using Loop In. It cannot support
At present, autoloop can be configured to either acquire incoming liquidity
using loop out, or acquire outgoing liquidity using loop in. It cannot support
automated swaps in both directions. To set the type of swaps you would like
to automatically dispatch, use:
```
loop setrule --type={in|out}
loop setparams --type={in|out}
```
Autoloop will perform Loop Out swaps *by default*.
Autoloop will perform loop out swaps *by default*.
Swaps that are dispatched by Autoloop can be identified in the output of
Swaps that are dispatched by the autolooper can be identified in the output of
`ListSwaps` by their label field, which will contain: `[reserved]: autoloop-out`.
Even if you do not choose to enable Autoloop, we encourage you to
Even if you do not choose to enable the autolooper, we encourage you to
experiment with setting the parameters described in this document because the
client will log the actions that it would have taken to provide visibility into
its functionality. Alternatively, the `SuggestSwaps` rpc (`loop suggestswaps`
on the CLI) provides a set of swaps that Autoloop currently recommends,
on the CLI) provides a set of swaps that the autolooper currently recommends,
which you can use to manually execute swaps if you'd like.
Note that autoloop parameters and rules are not persisted, so must be set on
@ -64,7 +64,7 @@ set a liquidity rule for a peer, you cannot also set a specific rule for one of
its channels.
### Liquidity Thresholds
To setup Autoloop to dispatch swaps on your behalf, you need to set the
To setup the autolooper to dispatch swaps on your behalf, you need to set the
liquidity balance you would like for each channel or peer. Desired liquidity
balance is expressed using threshold incoming and outgoing percentages of
capacity. The incoming threshold you specify indicates the minimum percentage
@ -73,7 +73,7 @@ threshold allows you to reserve a percentage of your balance for outgoing
capacity, but may be set to zero if you are only concerned with incoming
capacity.
Autoloop will perform swaps that push your incoming capacity to at least
The autolooper will perform swaps that push your incoming capacity to at least
the incoming threshold you specify, while reserving at least the outgoing
capacity threshold. Rules can be set as follows:
@ -93,14 +93,14 @@ to a percentage of the swap amount using the fee percentage parameter:
loop setparams --feepercent={percentage of swap amount}
```
If you are not using Easy Autoloop and you would like finer grained control over
swap fees, there are multiple fee related settings which can be used to tune
Autoloop to your preference. The sections that follow explain these settings
If you are not using easyautoloop and you would like finer grained control over
swap fees, there are multiple fee related settings which can be used to tune the
autolooper to your preference. The sections that follow explain these settings
in detail. Note that these fees are expressed on a per-swap basis, rather than
as an overall budget.
### On-Chain Fees
When performing a successful Loop Out swap, the Loop client needs to sweep the
When performing a successful loop out swap, the loop client needs to sweep the
on-chain HTLC sent by the server back into its own wallet.
#### Sweep Confirmation Target
@ -116,11 +116,11 @@ loop setparams --sweepconf={target in blocks}
#### Fee Market Awareness
The mempool often clears overnight, or on the weekends when fewer people are
using chain space. This is an opportune time for Autoloop to dispatch a
swap on your behalf while you sleep! Before dispatching a swap, Autoloop
using chain space. This is an opportune time for the autolooper to dispatch a
swap on your behalf while you sleep! Before dispatching a swap, the autolooper
will get a fee estimate for your on-chain sweep transaction (using its
`sweepconftarget`), and check it against the limit that has been configured.
The `sweeplimit` parameter can be set to configure Autoloop to only
The `sweeplimit` parameter can be set to configure the autolooper to only
dispatch in low-fee environments.
```
@ -130,7 +130,7 @@ loop setparams --sweeplimit={limit in sat/vbyte}
#### Miner Fee
In the event where fees spike dramatically right after a swap is dispatched,
it may not be worthwhile to proceed with the swap. The Loop client always uses
it may not be worthwhile to proceed with the swap. The loop client always uses
the latest fee estimation to sweep your swap within the desired target, but to
account for this edge case where fees dramatically spike for an extended period
of time, a maximum miner fee can be set to cap the amount that will be paid for
@ -141,7 +141,7 @@ loop setparams --maxminer={limit in satoshis}
### Server Fees
#### Swap Fee
The server charges a fee for facilitating swaps. Autoloop can be limited
The server charges a fee for facilitating swaps. The autolooper can be limited
to a set swap fee, expressed as a percentage of the total swap amount, using
the following command:
```
@ -154,13 +154,13 @@ costs. This value will only be charged if your client goes offline for a long
period of time after the server has published an on-chain HTLC and never
completes the swap, or if it decides to abort the swap due to high on-chain
fees. Both of these cases are unlikely, but this value can still be capped in
Autoloop.
the autolooper.
```
loop setparams --maxprepay={limit in satoshis}
```
### Off-Chain Fees
The Loop client dispatches two off-chain payments to the Loop server - one for
The loop client dispatches two off-chain payments to the loop server - one for
the swap prepayment, and one for the swap itself. The amount that the client
will pay in off-chain fees for each of these payments can be limited to a
percentage of the payment amount using the following commands:
@ -176,7 +176,7 @@ Swap routing fees:
```
## Budget
Autoloop operates within a set budget, and will stop executing swaps when
The autolooper operates within a set budget, and will stop executing swaps when
this budget is reached. This budget includes the fees paid to the swap server,
on-chain sweep costs and off-chain routing fees. Note that the budget does not
include the actual swap amount, as this balance is simply shifted from off-chain
@ -188,7 +188,7 @@ loop command:
loop setparams --autobudget={budget in satoshis}
```
Your Autoloop budget is refreshed based on a configurable interval. You can
Your autoloop budget is refreshed based on a configurable interval. You can
specify how often the budget is going to refresh by using the `setparams` loop
command:
```
@ -196,9 +196,9 @@ loop setparams --autobudgetrefreshperiod={duration}
```
If Autoloop has used up its budget, and you would like to top it up, you
If your autolooper has used up its budget, and you would like to top it up, you
can do so by either increasing the overall budget amount, or by decreasing the
refresh interval. For example, if you want to set Autoloop to
refresh interval. For example, if you want to set your autolooper to
have a budget of 100k sats every 168 hours, you could set the following:
```
loop setparams --autobudget=100000 --autobudgetrefreshperiod=168h
@ -206,15 +206,15 @@ loop setparams --autobudget=100000 --autobudgetrefreshperiod=168h
## Dispatch Control
Configuration options are also exposed to allow you to control the rate at
which swaps are automatically dispatched, and Autoloop's propensity to
which swaps are automatically dispatched, and the autolooper's propensity to
retry channels that have previously failed.
### In-flight Limit
The number of swaps that Autoloop will dispatch at a time is controlled
### In Flight Limit
The number of swaps that the autolooper will dispatch at a time is controlled
by the `autoinflight` parameter. The default value for this parameter is 1, and
can be increased if you would like to perform more automated swaps simultaneously.
If you have set a very high sweep target for your automatically dispatched swaps,
you may want to increase this value, because Autoloop will wait for the
you may want to increase this value, because the autolooper will wait for the
swap to fully complete, including the sweep confirming, before it dispatches
another swap.
@ -223,25 +223,25 @@ loop setparams --autoinflight=2
```
### Failure Backoff
Sometimes Loop Out swaps fail because they cannot find an off-chain route to the
Sometimes loop out swaps fail because they cannot find an off-chain route to the
server. This may happen because there is a temporary lack of liquidity along the
route, or because the peer that you need to perform a swap with simply does not
have a route to the Loop server's node. These swap attempts cost you nothing,
but we set a backoff period so that Autoloop will not continuously attempt
have a route to the loop server's node. These swap attempts cost you nothing,
but we set a backoff period so that the autolooper will not continuously attempt
to perform swaps through a very unbalanced channel that cannot facilitate a swap.
The default value for this parameter is 24 hours, and it can be updated as follows:
The default value for this parameter is 24hours, and it can be updated as follows:
```
loop setparams --failurebackoff={backoff in seconds}
```
### Swap Size
By default, Autoloop will execute a swap when the amount that needs to be
By default, the autolooper will execute a swap when the amount that needs to be
rebalanced within a channel is equal to the swap server's minimum swap size.
This means that it will dispatch swaps more regularly, and ensure that channels
are not run down too far below their configured threshold. If you are willing
to allow your liquidity to drop further than the minimum swap amount below your
threshold, a custom minimum swap size can be set. If Autoloop is configured
threshold, a custom minimum swap size can be set. If autolooper is configured
with a larger minimum swap size, it will allow channels to drop further below
their target threshold, but will perform fewer swaps, potentially saving on
fees.
@ -251,26 +251,26 @@ loop setparams --minamt={amount in satoshis}
```
Swaps are also limited to the maximum swap amount advertised by the server. If
you would like to reduce the size of swap that Autoloop created, this value can
you would like to reduce the size of swap that autoloop created, this value can
also be configured.
```
loop setparams --maxamt={amount in satoshis}
```
The server's current terms are provided by the `loop terms` CLI command. The
The server's current terms are provided by the `loop terms` cli command. The
values set for minimum and maximum swap amount must be within the range that
the server supports.
## Manual Swap Interaction
Autoloop will not dispatch swaps over channels that are already included
in manually dispatched swaps - for Loop Out, this would mean the channel is
The autolooper will not dispatch swaps over channels that are already included
in manually dispatched swaps - for loop out, this would mean the channel is
specified in the outgoing channel swap, and for loop in the channel's peer is
specified as the last hop for an ongoing swap. This check is put in place to
prevent Autoloop from interfering with swaps you have created yourself.
prevent the autolooper from interfering with swaps you have created yourself.
## Disqualified Swaps
There are various restrictions placed on the client's Autoloop functionality.
There are various restrictions placed on the client's autoloop functionality.
If a channel is not eligible for a swap at present, or it does not need one
based on the current set of liquidity rules, it will be listed in the
`Disqualified` section of the output of the `SuggestSwaps` API. One of the
@ -279,35 +279,35 @@ following reasons will be displayed:
* Budget not started: if the start date for your budget is in the future,
no swaps will be executed until the start date is reached. See [budget](#budget) to
update.
* Budget elapsed: if Autoloop has elapsed the budget assigned to it for
* Budget elapsed: if the autolooper has elapsed the budget assigned to it for
fees, this reason will be returned. See [budget](#budget) to update.
* Sweep fees: this reason will be displayed if the estimated chain fee rate for
sweeping a loop out swap is higher than the current limit. See [sweep fees](#fee-market-awareness)
to update.
* In-flight: there is a limit to the number of automatically dispatched swaps
* In flight: there is a limit to the number of automatically dispatched swaps
that the client allows. If this limit has been reached, no further swaps
will be automatically dispatched until the in-flight swaps complete. See
[in-flight limit](#in-flight-limit) to update.
[in flight limit](#in-flight-limit) to update.
* Budget insufficient: if there is not enough remaining budget for a swap,
including the amount currently reserved for in-flight swaps, an insufficient
including the amount currently reserved for in flight swaps, an insufficient
reason will be displayed. This differs from budget elapsed because there is
still budget remaining, just not enough to execute a specific swap.
* Swap fee: there is a limit placed on the fee that the client will pay to the
server for automatically dispatched swaps. The swap fee reason will be shown
if the fees advertised by the server are too high. See [swap fee](#swap-fee)
to update.
* Miner fee: if the estimated on-chain fees for a swap are too high, Autoloop
* Miner fee: if the estimated on-chain fees for a swap are too high, autoloop
will display a miner fee reason. See [miner fee](#miner-fee) to update.
* Prepay: if the no-show fee that the server will pay in the unlikely event
that the client fails to complete a swap is too high, a prepay reason will
be returned. See [no show fees](#no-show-fee) to update.
* Backoff: if an automatically dispatched swap has recently failed for a channel,
Autoloop will backoff for a period before retrying. See [failure backoff](#failure-backoff)
autoloop will backoff for a period before retrying. See [failure backoff](#failure-backoff)
to update.
* Loop Out: if there is currently a Loop Out swap in-flight on a channel, it
* Loop out: if there is currently a loop out swap in-flight on a channel, it
will not be used for automated swaps. This issue will resolve itself once the
in-flight swap completes.
* Loop In: if there is currently a Loop In swap in-flight for a peer, it will
* Loop in: if there is currently a loop in swap in-flight for a peer, it will
not be used for automated swaps. This will resolve itself once the swap is
completed.
* Liquidity ok: if a channel's current liquidity balance is within the bound set
@ -316,7 +316,7 @@ following reasons will be displayed:
* Fee insufficient: if the fees that a swap will cost are more than the
percentage of total swap amount that we allow, this reason will be displayed.
See [fees](#fees) to update this value.
* Loop In unreachable: if the client node is unreachable by the server
* Loop in unreachable: if the client node is unreachable by the server
off-chain, this reason will be displayed. Try improving the connectivity of
your node so that it is reachable by the loop server.

@ -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,48 +60,45 @@ 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 {
blockEpochChan, blockErrorChan, err =
s.lnd.ChainNotifier.RegisterBlockEpochNtfn(mainCtx)
if err != nil {
if strings.Contains(err.Error(),
"in the process of starting") {
if err == nil {
break
}
if strings.Contains(err.Error(),
"in the process of starting") {
log.Warnf("LND chain notifier server not ready yet, " +
"retrying with delay")
log.Warnf("LND chain notifier server not " +
"ready yet, retrying with delay")
// Give chain notifier some time to start and try to
// re-attempt block epoch subscription.
select {
case <-time.After(500 * time.Millisecond):
continue
// Give chain notifier some time to start and
// try to re-attempt block epoch subscription.
select {
case <-time.After(500 * time.Millisecond):
continue
case <-mainCtx.Done():
return err
case <-mainCtx.Done():
return err
}
}
return err
}
return err
break
}
// Before starting, make sure we have an up-to-date block height.
// Otherwise, we might reveal a preimage for a swap that is already
// Before starting, make sure we have an up to date block height.
// Otherwise we might reveal a preimage for a swap that is already
// expired.
log.Infof("Wait for first block notification")
log.Infof("Wait for first block ntfn")
var height int32
setHeight := func(h int32) {
@ -125,25 +115,10 @@ 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)
// Signal that executor being ready with an up-to-date block height.
// Signal that executor being ready with an up to date block height.
close(s.ready)
// Use a map to administer the individual notification queues for the
@ -173,33 +148,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 +193,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()
}

@ -1,127 +0,0 @@
package fsm
import (
"fmt"
)
// ExampleService is an example service that we want to wait for in the FSM.
type ExampleService interface {
WaitForStuffHappening() (<-chan bool, error)
}
// ExampleStore is an example store that we want to use in our exitFunc.
type ExampleStore interface {
StoreStuff() error
}
// ExampleFSM implements the FSM and uses the ExampleService and ExampleStore
// to implement the actions.
type ExampleFSM struct {
*StateMachine
service ExampleService
store ExampleStore
}
// NewExampleFSMContext creates a new example FSM context.
func NewExampleFSMContext(service ExampleService,
store ExampleStore) *ExampleFSM {
exampleFSM := &ExampleFSM{
service: service,
store: store,
}
exampleFSM.StateMachine = NewStateMachine(exampleFSM.GetStates(), 10)
return exampleFSM
}
// States.
const (
InitFSM = StateType("InitFSM")
StuffSentOut = StateType("StuffSentOut")
WaitingForStuff = StateType("WaitingForStuff")
StuffFailed = StateType("StuffFailed")
StuffSuccess = StateType("StuffSuccess")
)
// Events.
var (
OnRequestStuff = EventType("OnRequestStuff")
OnStuffSentOut = EventType("OnStuffSentOut")
OnStuffSuccess = EventType("OnStuffSuccess")
)
// GetStates returns the states for the example FSM.
func (e *ExampleFSM) GetStates() States {
return States{
EmptyState: State{
Transitions: Transitions{
OnRequestStuff: InitFSM,
},
},
InitFSM: State{
Action: e.initFSM,
Transitions: Transitions{
OnStuffSentOut: StuffSentOut,
OnError: StuffFailed,
},
},
StuffSentOut: State{
Action: e.waitForStuff,
Transitions: Transitions{
OnStuffSuccess: StuffSuccess,
OnError: StuffFailed,
},
},
StuffFailed: State{
Action: NoOpAction,
},
StuffSuccess: State{
Action: NoOpAction,
},
}
}
// InitStuffRequest is the event context for the InitFSM state.
type InitStuffRequest struct {
Stuff string
respondChan chan<- string
}
// initFSM is the action for the InitFSM state.
func (e *ExampleFSM) initFSM(eventCtx EventContext) EventType {
req, ok := eventCtx.(*InitStuffRequest)
if !ok {
return e.HandleError(
fmt.Errorf("invalid event context type: %T", eventCtx),
)
}
err := e.store.StoreStuff()
if err != nil {
return e.HandleError(err)
}
req.respondChan <- req.Stuff
return OnStuffSentOut
}
// waitForStuff is an action that waits for stuff to happen.
func (e *ExampleFSM) waitForStuff(eventCtx EventContext) EventType {
waitChan, err := e.service.WaitForStuffHappening()
if err != nil {
return e.HandleError(err)
}
go func() {
<-waitChan
err := e.SendEvent(OnStuffSuccess, nil)
if err != nil {
log.Errorf("unable to send event: %v", err)
}
}()
return NoOp
}

@ -1,12 +0,0 @@
```mermaid
stateDiagram-v2
[*] --> InitFSM: OnRequestStuff
InitFSM
InitFSM --> StuffSentOut: OnStuffSentOut
InitFSM --> StuffFailed: OnError
StuffFailed
StuffSentOut
StuffSentOut --> StuffSuccess: OnStuffSuccess
StuffSentOut --> StuffFailed: OnError
StuffSuccess
```

@ -1,324 +0,0 @@
package fsm
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/require"
)
var (
errService = errors.New("service error")
errStore = errors.New("store error")
)
type mockStore struct {
storeErr error
}
func (m *mockStore) StoreStuff() error {
return m.storeErr
}
type mockService struct {
respondChan chan bool
respondErr error
}
func (m *mockService) WaitForStuffHappening() (<-chan bool, error) {
return m.respondChan, m.respondErr
}
func newInitStuffRequest() *InitStuffRequest {
return &InitStuffRequest{
Stuff: "stuff",
respondChan: make(chan<- string, 1),
}
}
func TestExampleFSM(t *testing.T) {
testCases := []struct {
name string
expectedState StateType
eventCtx EventContext
expectedLastActionError error
sendEvent EventType
sendEventErr error
serviceErr error
storeErr error
}{
{
name: "success",
expectedState: StuffSuccess,
eventCtx: newInitStuffRequest(),
sendEvent: OnRequestStuff,
},
{
name: "service error",
expectedState: StuffFailed,
eventCtx: newInitStuffRequest(),
sendEvent: OnRequestStuff,
serviceErr: errService,
expectedLastActionError: errService,
},
{
name: "store error",
expectedLastActionError: errStore,
storeErr: errStore,
sendEvent: OnRequestStuff,
expectedState: StuffFailed,
eventCtx: newInitStuffRequest(),
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
respondChan := make(chan string, 1)
if req, ok := tc.eventCtx.(*InitStuffRequest); ok {
req.respondChan = respondChan
}
serviceResponseChan := make(chan bool, 1)
serviceResponseChan <- true
service := &mockService{
respondChan: serviceResponseChan,
respondErr: tc.serviceErr,
}
store := &mockStore{
storeErr: tc.storeErr,
}
exampleContext := NewExampleFSMContext(service, store)
cachedObserver := NewCachedObserver(100)
exampleContext.RegisterObserver(cachedObserver)
err := exampleContext.SendEvent(
tc.sendEvent, tc.eventCtx,
)
require.Equal(t, tc.sendEventErr, err)
require.Equal(
t,
tc.expectedLastActionError,
exampleContext.LastActionError,
)
err = cachedObserver.WaitForState(
context.Background(),
time.Second,
tc.expectedState,
)
require.NoError(t, err)
})
}
}
// getTestContext returns a test context for the example FSM and a cached
// observer that can be used to verify the state transitions.
func getTestContext() (*ExampleFSM, *CachedObserver) {
service := &mockService{
respondChan: make(chan bool, 1),
}
service.respondChan <- true
store := &mockStore{}
exampleContext := NewExampleFSMContext(service, store)
cachedObserver := NewCachedObserver(100)
exampleContext.RegisterObserver(cachedObserver)
return exampleContext, cachedObserver
}
// TestExampleFSMFlow tests different flows that the example FSM can go through.
func TestExampleFSMFlow(t *testing.T) {
testCases := []struct {
name string
expectedStateFlow []StateType
expectedEventFlow []EventType
storeError error
serviceError error
}{
{
name: "success",
expectedStateFlow: []StateType{
InitFSM,
StuffSentOut,
StuffSuccess,
},
expectedEventFlow: []EventType{
OnRequestStuff,
OnStuffSentOut,
OnStuffSuccess,
},
},
{
name: "failure on store",
expectedStateFlow: []StateType{
InitFSM,
StuffFailed,
},
expectedEventFlow: []EventType{
OnRequestStuff,
OnError,
},
storeError: errStore,
},
{
name: "failure on service",
expectedStateFlow: []StateType{
InitFSM,
StuffSentOut,
StuffFailed,
},
expectedEventFlow: []EventType{
OnRequestStuff,
OnStuffSentOut,
OnError,
},
serviceError: errService,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
exampleContext, cachedObserver := getTestContext()
if tc.storeError != nil {
exampleContext.store.(*mockStore).
storeErr = tc.storeError
}
if tc.serviceError != nil {
exampleContext.service.(*mockService).
respondErr = tc.serviceError
}
go func() {
err := exampleContext.SendEvent(
OnRequestStuff,
newInitStuffRequest(),
)
require.NoError(t, err)
}()
// Wait for the final state.
err := cachedObserver.WaitForState(
context.Background(),
time.Second,
tc.expectedStateFlow[len(
tc.expectedStateFlow,
)-1],
)
require.NoError(t, err)
allNotifications := cachedObserver.
GetCachedNotifications()
for index, notification := range allNotifications {
require.Equal(
t,
tc.expectedStateFlow[index],
notification.NextState,
)
require.Equal(
t,
tc.expectedEventFlow[index],
notification.Event,
)
}
})
}
}
// 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)
}
})
}
}

@ -1,341 +0,0 @@
package fsm
import (
"errors"
"fmt"
"sync"
)
// ErrEventRejected is the error returned when the state machine cannot process
// an event in the state that it is in.
var (
ErrEventRejected = errors.New("event rejected")
ErrWaitForStateTimedOut = errors.New(
"timed out while waiting for event",
)
ErrInvalidContextType = errors.New("invalid context")
ErrWaitingForStateEarlyAbortError = errors.New(
"waiting for state early abort",
)
)
const (
// EmptyState represents the default state of the system.
EmptyState StateType = ""
// NoOp represents a no-op event.
NoOp EventType = "NoOp"
// OnError can be used when an action returns a generic error.
OnError EventType = "OnError"
// ContextValidationFailed can be when the passed context if
// not of the expected type.
ContextValidationFailed EventType = "ContextValidationFailed"
)
// StateType represents an extensible state type in the state machine.
type StateType string
// EventType represents an extensible event type in the state machine.
type EventType string
// EventContext represents the context to be passed to the action
// implementation.
type EventContext interface{}
// Action represents the action to be executed in a given state.
type Action func(eventCtx EventContext) EventType
// Transitions represents a mapping of events and states.
type Transitions map[EventType]StateType
// State binds a state with an action and a set of events it can handle.
type State struct {
// EntryFunc is a function that is called when the state is entered.
EntryFunc func()
// ExitFunc is a function that is called when the state is exited.
ExitFunc func()
// Action is the action to be executed in the state.
Action Action
// Transitions is a mapping of events and states.
Transitions Transitions
}
// States represents a mapping of states and their implementations.
type States map[StateType]State
// Notification represents a notification sent to the state machine's
// notification channel.
type Notification struct {
// PreviousState is the state the state machine was in before the event
// was processed.
PreviousState StateType
// NextState is the state the state machine is in after the event was
// processed.
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
// observe the state machine.
type Observer interface {
Notify(Notification)
}
// StateMachine represents the state machine.
type StateMachine struct {
// Context represents the state machine context.
States States
// ActionEntryFunc is a function that is called before an action is
// executed.
ActionEntryFunc func(Notification)
// 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)
// 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
// current represents the current state.
current StateType
// observers is a slice of observers that are notified when the state
// machine transitions between states.
observers []Observer
// 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)
}
return &StateMachine{
States: states,
current: current,
DefaultObserver: defaultObserver,
observers: observers,
}
}
// getNextState returns the next state for the event given the machine's current
// state, or an error if the event can't be handled in the given state.
func (s *StateMachine) getNextState(event EventType) (State, error) {
var (
state State
ok bool
)
stateMap := s.States
if state, ok = stateMap[s.current]; !ok {
return State{}, NewErrConfigError("current state not found")
}
if state.Transitions == nil {
return State{}, NewErrConfigError(
"current state has no transitions",
)
}
var next StateType
if next, ok = state.Transitions[event]; !ok {
return State{}, NewErrConfigError(
"event not found in current transitions",
)
}
// Identify the state definition for the next state.
state, ok = stateMap[next]
if !ok {
return State{}, NewErrConfigError("next state not found")
}
if state.Action == nil {
return State{}, NewErrConfigError("next state has no action")
}
// Transition over to the next state.
s.previous = s.current
s.current = next
return state, nil
}
// SendEvent sends an event to the state machine. It returns an error if the
// event cannot be processed in the current state. Otherwise, it only returns
// nil if the event for the last action is a no-op.
func (s *StateMachine) SendEvent(event EventType, eventCtx EventContext) error {
s.mutex.Lock()
defer s.mutex.Unlock()
if s.States == nil {
return NewErrConfigError("state machine config is nil")
}
for {
// Determine the next state for the event given the machine's
// 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)
}
s.observerMutex.Unlock()
// Execute the state machines ActionEntryFunc.
if s.ActionEntryFunc != nil {
s.ActionEntryFunc(notification)
}
// Execute the current state's entry function
if state.EntryFunc != nil {
state.EntryFunc()
}
// Execute the next state's action and loop over again if the
// event returned is not a no-op.
nextEvent := state.Action(eventCtx)
// Execute the current state's exit function
if state.ExitFunc != nil {
state.ExitFunc()
}
// Execute the state machines ActionExitFunc.
if s.ActionExitFunc != nil {
s.ActionExitFunc(nextEvent)
}
// If the next event is a no-op, we're done.
if nextEvent == NoOp {
return nil
}
event = nextEvent
}
}
// RegisterObserver registers an observer with the state machine.
func (s *StateMachine) RegisterObserver(observer Observer) {
s.observerMutex.Lock()
defer s.observerMutex.Unlock()
if observer != nil {
s.observers = append(s.observers, observer)
}
}
// RemoveObserver removes an observer from the state machine. It returns true
// if the observer was removed, false otherwise.
func (s *StateMachine) RemoveObserver(observer Observer) bool {
s.observerMutex.Lock()
defer s.observerMutex.Unlock()
for i, o := range s.observers {
if o == observer {
s.observers = append(
s.observers[:i], s.observers[i+1:]...,
)
return true
}
}
return false
}
// HandleError is a helper function that can be used by actions to handle
// errors.
func (s *StateMachine) HandleError(err error) EventType {
log.Errorf("StateMachine error: %s", err)
s.LastActionError = err
return OnError
}
// NoOpAction is a no-op action that can be used by states that don't need to
// execute any action.
func NoOpAction(_ EventContext) EventType {
return NoOp
}
// 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)
}
// NewErrConfigError creates a new ErrConfigError.
func NewErrConfigError(msg string) ErrConfigError {
return ErrConfigError{
msg: 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)
}
// NewErrWaitingForStateTimeout creates a new ErrWaitingForStateTimeout.
func NewErrWaitingForStateTimeout(expected StateType) ErrWaitingForStateTimeout {
return ErrWaitingForStateTimeout{
expected: expected,
}
}

@ -1,139 +0,0 @@
# Finite State Machine Module
This module provides a simple golang finite state machine (FSM) implementation.
## Introduction
The state machine uses events and actions to transition between states. The
events are used to trigger a transition and the actions are used to perform
some work when entering a state. Actions return new events which are then
used to trigger the next transition.
## Usage
A simple way to use the FSM is to embed it into a struct:
```go
type LightSwitchFSM struct {
*StateMachine
}
```
In order to use the FSM you need to define the events, actions and statemaps
for the FSM. events are defined as constants, actions are defined as functions
on the `LightSwitchFSM` struct and statemaps are in a map of `State` to `StateMap`
where `StateMap` is a map of `Event` to `Action`.
For the `LightSwitchFSM` we can first define the states
```go
const (
OffState = StateType("Off")
OnState = StateType("On")
)
const (
SwitchOff = EventType("SwitchOff")
SwitchOn = EventType("SwitchOn")
)
```
Next we define the actions, here we're simply going to log from the action.
```go
func (a *LightSwitchFSM) OffAction(_ EventContext) EventType {
fmt.Println("The light has been switched off")
return NoOp
}
func (a *LightSwitchFSM) OnAction(_ EventContext) EventType {
fmt.Println("The light has been switched on")
return NoOp
}
```
Next we define the statemap, here we're going to implement a getStates()
function that returns the statemap.
```go
func (l *LightSwitchFSM) getStates() States {
return States{
OffState: State{
Action: l.OffAction,
Transitions: Transitions{
SwitchOn: OnState,
},
},
OnState: State{
Action: l.OnAction,
Transitions: Transitions{
SwitchOff: OffState,
},
},
}
}
```
Finally, we can create the FSM and use it.
```go
func NewLightSwitchFSM() *LightSwitchFSM {
fsm := &LightSwitchFSM{}
fsm.StateMachine = &StateMachine{
States: fsm.getStates(),
Current: OffState,
}
return fsm
}
```
This is what it would look like to use the FSM:
```go
func TestLightSwitchFSM(t *testing.T) {
// Create a new light switch FSM.
lightSwitch := NewLightSwitchFSM()
// Expect the light to be off
require.Equal(t, lightSwitch.Current, OffState)
// Send the On Event
err := lightSwitch.SendEvent(SwitchOn, nil)
require.NoError(t, err)
// Expect the light to be on
require.Equal(t, lightSwitch.Current, OnState)
// Send the Off Event
err = lightSwitch.SendEvent(SwitchOff, nil)
require.NoError(t, err)
// Expect the light to be off
require.Equal(t, lightSwitch.Current, OffState)
}
```
## Observing the state machine
The state machine can be observed by registering an observer. The observer
will be called when the state machine transitions between states. The observer
is called with the old state, the new state and the event that triggered the
transition.
An observer can be registered by calling the `RegisterObserver` function on
the state machine. The observer must implement the `Observer` interface.
```go
type Observer interface {
Notify(Notification)
}
```
An example of a cached observer can be found in [observer.go](./observer.go).
## More Examples
A more elaborate example that uses error handling, event context and more
elaborate actions can be found in here [examples_fsm.go](./example_fsm.go).
With the tests in [examples_fsm_test.go](./example_fsm_test.go) showing how to
use the FSM.
## Visualizing the FSM
The FSM can be visualized to mermaid markdown using the [stateparser.go](./stateparser/stateparser.go)
tool. The visualization for the exampleFSM can be found in [example_fsm.md](./example_fsm.md).

@ -1,118 +0,0 @@
package fsm
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
)
var (
errAction = errors.New("action error")
)
// TestStateMachineContext is a test context for the state machine.
type TestStateMachineContext struct {
*StateMachine
}
// GetStates returns the states for the test state machine.
// The StateMap looks like this:
// State1 -> Event1 -> State2 .
func (c *TestStateMachineContext) GetStates() States {
return States{
"State1": State{
Action: func(ctx EventContext) EventType {
return "Event1"
},
Transitions: Transitions{
"Event1": "State2",
},
},
"State2": State{
Action: func(ctx EventContext) EventType {
return "NoOp"
},
Transitions: Transitions{},
},
}
}
// errorAction returns an error.
func (c *TestStateMachineContext) errorAction(eventCtx EventContext) EventType {
return c.StateMachine.HandleError(errAction)
}
func setupTestStateMachineContext() *TestStateMachineContext {
ctx := &TestStateMachineContext{}
ctx.StateMachine = &StateMachine{
States: ctx.GetStates(),
current: "State1",
previous: "",
}
return ctx
}
// TestStateMachine_Success tests the state machine with a successful event.
func TestStateMachine_Success(t *testing.T) {
ctx := setupTestStateMachineContext()
// Send an event to the state machine.
err := ctx.SendEvent("Event1", nil)
require.NoError(t, err)
// Check that the state machine has transitioned to the next state.
require.Equal(t, StateType("State2"), ctx.current)
}
// TestStateMachine_ConfigurationError tests the state machine with a
// configuration error.
func TestStateMachine_ConfigurationError(t *testing.T) {
ctx := setupTestStateMachineContext()
ctx.StateMachine.States = nil
err := ctx.SendEvent("Event1", nil)
require.EqualError(
t, err,
NewErrConfigError("state machine config is nil").Error(),
)
}
// TestStateMachine_ActionError tests the state machine with an action error.
func TestStateMachine_ActionError(t *testing.T) {
ctx := setupTestStateMachineContext()
states := ctx.StateMachine.States
// Add a Transition to State2 if the Action on Stat2 fails.
// The new StateMap looks like this:
// State1 -> Event1 -> State2
//
// State2 -> OnError -> ErrorState
states["State2"] = State{
Action: ctx.errorAction,
Transitions: Transitions{
OnError: "ErrorState",
},
}
states["ErrorState"] = State{
Action: func(ctx EventContext) EventType {
return "NoOp"
},
Transitions: Transitions{},
}
err := ctx.SendEvent("Event1", nil)
// Sending an event to the state machine should not return an error.
require.NoError(t, err)
// Ensure that the last error is set.
require.Equal(t, errAction, ctx.StateMachine.LastActionError)
// Expect the state machine to have transitioned to the ErrorState.
require.Equal(t, StateType("ErrorState"), ctx.StateMachine.current)
}

@ -1,26 +0,0 @@
package fsm
import (
"github.com/btcsuite/btclog"
"github.com/lightningnetwork/lnd/build"
)
// Subsystem defines the sub system name of this package.
const Subsystem = "FSM"
// 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,242 +0,0 @@
package fsm
import (
"context"
"sync"
"time"
)
// CachedObserver is an observer that caches all states and transitions of
// the observed state machine.
type CachedObserver struct {
lastNotification Notification
cachedNotifications *FixedSizeSlice[Notification]
notificationCond *sync.Cond
notificationMx sync.Mutex
}
// NewCachedObserver creates a new cached observer with the given maximum
// number of cached notifications.
func NewCachedObserver(maxElements int) *CachedObserver {
fixedSizeSlice := NewFixedSizeSlice[Notification](maxElements)
observer := &CachedObserver{
cachedNotifications: fixedSizeSlice,
}
observer.notificationCond = sync.NewCond(&observer.notificationMx)
return observer
}
// Notify implements the Observer interface.
func (c *CachedObserver) Notify(notification Notification) {
c.notificationMx.Lock()
defer c.notificationMx.Unlock()
c.cachedNotifications.Add(notification)
c.lastNotification = notification
c.notificationCond.Broadcast()
}
// GetCachedNotifications returns a copy of the cached notifications.
func (c *CachedObserver) GetCachedNotifications() []Notification {
c.notificationMx.Lock()
defer c.notificationMx.Unlock()
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)
}
// 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
}
}
// 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.
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:
}
}
for {
// Check if the last state is the desired state.
if c.lastNotification.NextState == state {
writeResult(nil)
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()
}
}()
return ch
}
// FixedSizeSlice is a slice with a fixed size.
type FixedSizeSlice[T any] struct {
data []T
maxLen int
sync.Mutex
}
// NewFixedSlice initializes a new FixedSlice with a given maximum length.
func NewFixedSizeSlice[T any](maxLen int) *FixedSizeSlice[T] {
return &FixedSizeSlice[T]{
data: make([]T, 0, maxLen),
maxLen: maxLen,
}
}
// Add appends a new element to the slice. If the slice reaches its maximum
// length, the first element is removed.
func (fs *FixedSizeSlice[T]) Add(element T) {
fs.Lock()
defer fs.Unlock()
if len(fs.data) == fs.maxLen {
// Remove the first element
fs.data = fs.data[1:]
}
// Add the new element
fs.data = append(fs.data, element)
}
// Get returns a copy of the slice.
func (fs *FixedSizeSlice[T]) Get() []T {
fs.Lock()
defer fs.Unlock()
data := make([]T, len(fs.data))
copy(data, fs.data)
return data
}
// GetElement returns the element at the given index.
func (fs *FixedSizeSlice[T]) GetElement(index int) T {
fs.Lock()
defer fs.Unlock()
return fs.data[index]
}

@ -1,112 +0,0 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"github.com/lightninglabs/loop/fsm"
"github.com/lightninglabs/loop/instantout"
"github.com/lightninglabs/loop/instantout/reservation"
)
func main() {
if err := run(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func run() error {
out := flag.String("out", "", "outfile")
stateMachine := flag.String("fsm", "", "the swap state machine to parse")
flag.Parse()
if filepath.Ext(*out) != ".md" {
return errors.New("wrong argument: out must be a .md file")
}
fp, err := filepath.Abs(*out)
if err != nil {
return err
}
switch *stateMachine {
case "example":
exampleFSM := &fsm.ExampleFSM{}
err = writeMermaidFile(fp, exampleFSM.GetStates())
if err != nil {
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")
fmt.Println("\texample")
}
return nil
}
func writeMermaidFile(filename string, states fsm.States) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
var b bytes.Buffer
fmt.Fprint(&b, "```mermaid\nstateDiagram-v2\n")
sortedStates := sortedKeys(states)
for _, state := range sortedStates {
edges := states[fsm.StateType(state)]
// write state name
if len(state) > 0 {
fmt.Fprintf(&b, "%s\n", state)
} else {
state = "[*]"
}
// write transitions
for edge, target := range edges.Transitions {
fmt.Fprintf(&b, "%s --> %s: %s\n", state, target, edge)
}
}
fmt.Fprint(&b, "```")
_, err = f.Write(b.Bytes())
if err != nil {
return err
}
return nil
}
func sortedKeys(m fsm.States) []string {
keys := make([]string, len(m))
i := 0
for k := range m {
keys[i] = string(k)
i++
}
sort.Strings(keys)
return keys
}

199
go.mod

@ -1,153 +1,159 @@
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/btcutil/psbt v1.1.8
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/btcsuite/btcd v0.23.5-0.20230125025938-be056b0a0b2f
github.com/btcsuite/btcd/btcec/v2 v2.3.2
github.com/btcsuite/btcd/btcutil v1.1.3
github.com/btcsuite/btcd/btcutil/psbt v1.1.5
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.7
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/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/golang-migrate/migrate/v4 v4.15.2
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0
github.com/jackc/pgconn v1.10.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/lightninglabs/loop/swapserverrpc v1.0.5
github.com/lightningnetwork/lnd v0.18.0-beta.1
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/lib/pq v1.10.3
github.com/lightninglabs/aperture v0.1.20-beta
github.com/lightninglabs/lndclient v0.16.0-10
github.com/lightninglabs/loop/swapserverrpc v1.0.4
github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display
github.com/lightningnetwork/lnd v0.16.0-beta
github.com/lightningnetwork/lnd/cert v1.2.1
github.com/lightningnetwork/lnd/clock v1.1.0
github.com/lightningnetwork/lnd/queue v1.1.0
github.com/lightningnetwork/lnd/ticker v1.1.0
github.com/lightningnetwork/lnd/tor v1.1.0
github.com/ory/dockertest/v3 v3.10.0
github.com/stretchr/testify v1.9.0
github.com/stretchr/testify v1.8.1
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
gopkg.in/macaroon-bakery.v2 v2.1.0
golang.org/x/net v0.8.0
google.golang.org/grpc v1.53.0
google.golang.org/protobuf v1.28.1
gopkg.in/macaroon-bakery.v2 v2.0.1
gopkg.in/macaroon.v2 v2.1.0
modernc.org/sqlite v1.29.8
modernc.org/sqlite v1.20.3
)
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
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
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
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/docker/go-units v0.4.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/imdario/mergo v0.3.12 // 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/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/pgproto3/v2 v2.1.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.8.1 // indirect
github.com/jackc/pgx/v4 v4.13.0 // 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/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/lightninglabs/neutrino v0.15.0 // indirect
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect
github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect
github.com/lightningnetwork/lnd/kvdb v1.4.1 // indirect
github.com/lightningnetwork/lnd/tlv v1.1.0 // 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.16 // 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/moby/term v0.0.0-20210619224110-3f7ff695adc6 // 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/sirupsen/logrus v1.8.1 // 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.10 // 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
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.etcd.io/bbolt v1.3.7 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.etcd.io/etcd/api/v3 v3.5.7 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect
go.etcd.io/etcd/client/v2 v2.305.7 // indirect
@ -155,47 +161,48 @@ 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.11.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.1.0 // indirect
golang.org/x/exp v0.0.0-20221111094246-ab4555d3164f // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
golang.org/x/tools v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20220314164441-57ef72a4c106 // 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
// We need to use grpc v1.39.0 because of a change in how HTTP errors are
// formatted and sent to the client. This change was introduced in grpc v1.40.0
// with https://github.com/grpc/grpc-go/pull/4474.
replace google.golang.org/grpc => google.golang.org/grpc v1.39.0
replace github.com/lightninglabs/loop/swapserverrpc => ./swapserverrpc
go 1.22.3
go 1.18

2285
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(loopdb.NewTypedStore[Querier](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,308 +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"
)
// Querier is the interface that contains all the queries generated
// by sqlc for the reservation table.
type Querier 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)
// InsertReservationUpdate inserts a new reservation update.
InsertReservationUpdate(ctx context.Context,
arg sqlc.InsertReservationUpdateParams) error
// UpdateReservation updates a reservation.
UpdateReservation(ctx context.Context,
arg sqlc.UpdateReservationParams) error
}
// BaseDB is the interface that contains all the queries generated
// by sqlc for the reservation table and transaction functionality.
type BaseDB interface {
Querier
// ExecTx allows for executing a function in the context of a database
// transaction.
ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
txBody func(Querier) 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.NewSqlWriteOpts(),
func(q Querier) 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.NewSqlWriteOpts(),
func(q Querier) 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 Querier) 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 Querier) 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(loopdb.NewTypedStore[Querier](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,438 +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"
)
// Querier is the interface that contains all the queries generated
// by sqlc for the instantout table.
type Querier 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)
}
// InstantOutBaseDB is the interface that contains all the queries generated
// by sqlc for the instantout table and transaction functionality.
type InstantOutBaseDB interface {
Querier
// ExecTx allows for executing a function in the context of a database
// transaction.
ExecTx(ctx context.Context, txOptions loopdb.TxOptions,
txBody func(Querier) 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.NewSqlWriteOpts(),
func(q Querier) 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.NewSqlWriteOpts(),
func(q Querier) 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,
@ -986,7 +984,7 @@ func TestAutoloopBothTypes(t *testing.T) {
Events: []*loopdb.LoopEvent{
{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateSuccess,
State: loopdb.SwapState(loopdb.StateSuccess),
},
},
},
@ -1170,7 +1168,7 @@ func TestAutoLoopRecurringBudget(t *testing.T) {
Events: []*loopdb.LoopEvent{
{
SwapStateData: loopdb.SwapStateData{
State: loopdb.StateSuccess,
State: loopdb.SwapState(loopdb.StateSuccess),
},
},
},
@ -1258,14 +1256,6 @@ func TestAutoLoopRecurringBudget(t *testing.T) {
func TestEasyAutoloop(t *testing.T) {
defer test.Guard(t)
// Decode a dummy p2wkh address to use as the destination address for
// the swaps.
p2wkhAddr := "bcrt1qq68r6ff4k4pjx39efs44gcyccf7unqnu5qtjjz"
addr, err := btcutil.DecodeAddress(p2wkhAddr, nil)
if err != nil {
t.Error(err)
}
// We need to change the default channels we use for tests so that they
// have different local balances in order to know which one is going to
// be selected by easy autoloop.
@ -1294,7 +1284,6 @@ func TestEasyAutoloop(t *testing.T) {
params = Parameters{
Autoloop: true,
DestAddr: addr,
AutoFeeBudget: 36000,
AutoFeeRefreshPeriod: time.Hour * 3,
AutoloopBudgetLastRefresh: testBudgetStart,
@ -1316,7 +1305,6 @@ func TestEasyAutoloop(t *testing.T) {
chan1Swap = &loop.OutRequest{
Amount: btcutil.Amount(maxAmt),
DestAddr: addr,
OutgoingChanSet: loopdb.ChannelSet{easyChannel1.ChannelID},
Label: labels.AutoloopLabel(swap.TypeOut),
Initiator: autoloopSwapInitiator,
@ -1364,9 +1352,6 @@ func TestEasyAutoloop(t *testing.T) {
easyChannel1, easyChannel2,
}
// Remove the custom dest address.
params.DestAddr = nil
c = newAutoloopTestCtx(t, params, channels, testRestrictions)
c.start()

@ -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,
)
@ -419,11 +402,6 @@ func (c *autoloopTestCtx) easyautoloop(step *easyAutoloopStep, noop bool) {
c.t, expected.request.OutgoingChanSet,
actual.OutgoingChanSet,
)
if expected.request.DestAddr != nil {
require.Equal(
c.t, expected.request.DestAddr, actual.DestAddr,
)
}
}
// Since we're checking if any false-positive swaps were dispatched we

@ -349,7 +349,7 @@ func (f *FeePortion) loopOutLimits(swapAmt btcutil.Amount,
// multiplier from the miner fees. We do this because we want to
// consider the average case for our budget calculations and not the
// severe edge-case miner fees.
miner /= maxMinerMultiplier
miner = miner / maxMinerMultiplier
// Calculate the worst case fees that we could pay for this swap,
// ensuring that we are within our fee limit even if the swap fails.

@ -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 (
@ -453,7 +454,6 @@ func (m *Manager) autoloop(ctx context.Context) error {
// outs.
if m.params.DestAddr != nil {
swap.DestAddr = m.params.DestAddr
swap.IsExternalAddr = true
}
go m.dispatchStickyLoopOut(
@ -536,7 +536,10 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
// Get a summary of our existing swaps so that we can check our autoloop
// budget.
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
summary, err := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
if err != nil {
return err
}
err = m.checkSummaryBudget(summary)
if err != nil {
@ -578,7 +581,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
// allowed clamp it to max.
amount := localTotal - m.params.EasyAutoloopTarget
if amount > restrictions.Maximum {
amount = restrictions.Maximum
amount = btcutil.Amount(restrictions.Maximum)
}
// If the amount we want to loop out is less than the minimum we can't
@ -598,7 +601,7 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
builder := newLoopOutBuilder(m.cfg)
channel := m.pickEasyAutoloopChannel(
channels, restrictions, loopOut, loopIn,
channels, restrictions, loopOut, loopIn, amount,
)
if channel == nil {
return fmt.Errorf("no eligible channel for easy autoloop")
@ -621,12 +624,12 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
switch feeLimit := easyParams.FeeLimit.(type) {
case *FeePortion:
if feeLimit.PartsPerMillion == 0 {
easyParams.FeeLimit = &FeePortion{
feeLimit = &FeePortion{
PartsPerMillion: defaultFeePPM,
}
}
default:
easyParams.FeeLimit = &FeePortion{
feeLimit = &FeePortion{
PartsPerMillion: defaultFeePPM,
}
}
@ -643,16 +646,16 @@ func (m *Manager) dispatchBestEasyAutoloopSwap(ctx context.Context) error {
return err
}
var swp loop.OutRequest
swap := loop.OutRequest{}
if t, ok := suggestion.(*loopOutSwapSuggestion); ok {
swp = t.OutRequest
swap = t.OutRequest
} else {
return fmt.Errorf("unexpected swap suggestion type: %T", t)
}
// Dispatch a sticky loop out.
go m.dispatchStickyLoopOut(
ctx, swp, defaultAmountBackoffRetry, defaultAmountBackoff,
ctx, swap, defaultAmountBackoffRetry, defaultAmountBackoff,
)
return nil
@ -759,7 +762,10 @@ func (m *Manager) SuggestSwaps(ctx context.Context) (
// Get a summary of our existing swaps so that we can check our autoloop
// budget.
summary := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
summary, err := m.checkExistingAutoLoops(ctx, loopOut, loopIn)
if err != nil {
return nil, err
}
err = m.checkSummaryBudget(summary)
if err != nil {
@ -1068,9 +1074,9 @@ func (e *existingAutoLoopSummary) totalFees() btcutil.Amount {
// automatically dispatched swaps that have completed, and the worst-case fee
// total for our set of ongoing, automatically dispatched swaps as well as a
// current in-flight count.
func (m *Manager) checkExistingAutoLoops(_ context.Context,
loopOuts []*loopdb.LoopOut,
loopIns []*loopdb.LoopIn) *existingAutoLoopSummary {
func (m *Manager) checkExistingAutoLoops(ctx context.Context,
loopOuts []*loopdb.LoopOut, loopIns []*loopdb.LoopIn) (
*existingAutoLoopSummary, error) {
var summary existingAutoLoopSummary
@ -1127,7 +1133,7 @@ func (m *Manager) checkExistingAutoLoops(_ context.Context,
}
}
return &summary
return &summary, nil
}
// currentSwapTraffic examines our existing swaps and returns a summary of the
@ -1199,14 +1205,8 @@ func (m *Manager) currentSwapTraffic(loopOut []*loopdb.LoopOut,
pubkey := *in.Contract.LastHop
switch {
// Include any pending swaps in our ongoing set of swaps. Swaps
// that reached InvoiceSettled are not considered ongoing since
// from the client's perspective the swap is complete. This
// consideration allows the client to dispatch the next autoloop
// in once an invoice for a previous swap is settled.
case in.State().State.Type() == loopdb.StateTypePending &&
in.State().State != loopdb.StateInvoiceSettled:
// Include any pending swaps in our ongoing set of swaps.
case in.State().State.Type() == loopdb.StateTypePending:
traffic.ongoingLoopIn[pubkey] = true
// If a swap failed with an on-chain timeout, the server could
@ -1410,7 +1410,7 @@ func (m *Manager) waitForSwapPayment(ctx context.Context, swapHash lntypes.Hash,
// swap conflicts.
func (m *Manager) pickEasyAutoloopChannel(channels []lndclient.ChannelInfo,
restrictions *Restrictions, loopOut []*loopdb.LoopOut,
loopIn []*loopdb.LoopIn) *lndclient.ChannelInfo {
loopIn []*loopdb.LoopIn, amount btcutil.Amount) *lndclient.ChannelInfo {
traffic := m.currentSwapTraffic(loopOut, loopIn)
@ -1466,6 +1466,7 @@ func (m *Manager) numActiveStickyLoops() int {
defer m.activeStickyLock.Unlock()
return m.activeStickyLoops
}
func (m *Manager) checkSummaryBudget(summary *existingAutoLoopSummary) error {
@ -1475,6 +1476,7 @@ func (m *Manager) checkSummaryBudget(summary *existingAutoLoopSummary) error {
"(upper limit)",
m.params.AutoFeeBudget, summary.spentFees,
summary.pendingFees)
}
return nil
@ -1548,3 +1550,7 @@ func satPerKwToSatPerVByte(satPerKw chainfee.SatPerKWeight) int64 {
func ppmToSat(amount btcutil.Amount, ppm uint64) btcutil.Amount {
return btcutil.Amount(uint64(amount) * ppm / FeeBase)
}
func mSatToSatoshis(amount lnwire.MilliSatoshi) btcutil.Amount {
return btcutil.Amount(amount / 1000)
}

@ -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,20 +160,15 @@ 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,
)
if err != nil {
return nil, err
}
request.DestAddr = addr
addr, err := b.cfg.Lnd.WalletKit.NextAddr(
ctx, account, addrType, false,
)
if err != nil {
return nil, err
}
request.DestAddr = addr
}
return &loopOutSwapSuggestion{

@ -3,17 +3,18 @@ package liquidity
import (
"errors"
"fmt"
"github.com/lightningnetwork/lnd/lnrpc/walletrpc"
"strings"
"time"
"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 (
@ -526,6 +527,7 @@ func ParametersToRpc(cfg Parameters) (*clientrpc.LiquidityParameters,
default:
addrType = clientrpc.AddressType_ADDRESS_TYPE_UNKNOWN
}
rpcCfg := &clientrpc.LiquidityParameters{

@ -127,10 +127,10 @@ func (r *ThresholdRule) swapAmount(channel *balances,
// This function can be used for loop out or loop in, but the concept is the
// same - we want liquidity in one (target) direction, while preserving some
// minimum in the other (reserve) direction.
// - target: this is the side of the channel(s) where we want to acquire some
// liquidity. We aim for this liquidity to reach the threshold amount set.
// - reserve: this is the side of the channel(s) that we will move liquidity
// away from. This may not drop below a certain reserve threshold.
// * target: this is the side of the channel(s) where we want to acquire some
// liquidity. We aim for this liquidity to reach the threshold amount set.
// * reserve: this is the side of the channel(s) that we will move liquidity
// away from. This may not drop below a certain reserve threshold.
func calculateSwapAmount(targetAmount, reserveAmount,
capacity btcutil.Amount, targetThresholdPercentage,
reserveThresholdPercentage uint64) btcutil.Amount {

@ -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,
},
}
}

@ -10,20 +10,14 @@ import (
"strings"
"sync"
"sync/atomic"
"time"
"github.com/coreos/bbolt"
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 +45,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)
}
@ -101,7 +95,7 @@ func New(config *Config, lisCfg *ListenerCfg) *Daemon {
// We send exactly one error on this channel if something goes
// wrong at runtime. Or a nil value if the shutdown was
// successful. But in case nobody's listening, we don't want to
// block on it, so we buffer it.
// block on it so we buffer it.
ErrChan: make(chan error, 1),
quit: make(chan struct{}),
@ -118,9 +112,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,15 +129,15 @@ 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) {
// We're trying to be started as a standalone Loop daemon, most
// likely LiT is already running and blocking the DB
return fmt.Errorf("%v: make sure no other loop daemon process "+
"(standalone or embedded in lightning-terminal) is"+
"running", err)
return fmt.Errorf("%v: make sure no other loop daemon "+
"process (standalone or embedded in "+
"lightning-terminal) is running", err)
}
if err != nil {
return err
@ -172,9 +166,9 @@ func (d *Daemon) Start() error {
func (d *Daemon) StartAsSubserver(lndGrpc *lndclient.GrpcLndServices,
withMacaroonService bool) 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
}
@ -185,8 +179,8 @@ func (d *Daemon) StartAsSubserver(lndGrpc *lndclient.GrpcLndServices,
// With lnd already pre-connected, initialize everything else, such as
// the swap server client, the RPC server instance and our main swap
// handlers. If this fails, then nothing has been started yet, and we
// can just return the error.
// handlers. If this fails, then nothing has been started yet and we can
// just return the error.
err := d.initialize(withMacaroonService)
if errors.Is(err, bbolt.ErrTimeout) {
// We're trying to be started inside LiT so there most likely is
@ -231,7 +225,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 +285,7 @@ func (d *Daemon) startWebServers() error {
restProxyDest, "[::]", "[::1]", 1,
)
}
err = loop_looprpc.RegisterSwapClientHandlerFromEndpoint(
err = looprpc.RegisterSwapClientHandlerFromEndpoint(
ctx, mux, restProxyDest, proxyOpts,
)
if err != nil {
@ -308,10 +302,7 @@ func (d *Daemon) startWebServers() error {
if d.restListener != nil {
log.Infof("Starting REST proxy listener")
d.restServer = &http.Server{
Handler: restHandler,
ReadHeaderTimeout: 5 * time.Second,
}
d.restServer = &http.Server{Handler: restHandler}
d.wg.Add(1)
go func() {
@ -322,7 +313,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 +332,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
@ -379,8 +370,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
}
}
// 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.
// Both the client RPC server and 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())
log.Infof("Swap server address: %v", d.cfg.Server.Host)
@ -397,54 +388,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(
loopdb.NewTypedStore[sweepbatcher.Querier](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 {
@ -498,63 +448,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(
loopdb.NewTypedStore[reservation.Querier](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(
loopdb.NewTypedStore[instantout.Querier](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.
@ -614,76 +518,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
@ -694,9 +535,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: "+
@ -705,7 +546,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()
@ -736,7 +577,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,11 @@ 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 +30,12 @@ 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)
@ -40,7 +61,7 @@ func migrateBoltdb(ctx context.Context, cfg *Config) error {
return err
}
// If the migration was successful we'll rename the bolt db to
// If the migration was successfull we'll rename the bolt db to
// loop.db.bk.
err = os.Rename(
filepath.Join(cfg.DataDir, "loop.db"),
@ -61,17 +82,6 @@ func needSqlMigration(cfg *Config) bool {
return false
}
// Do not migrate if sqlite db already exists. This is to prevent the
// migration from running multiple times for systems that may restore
// any deleted files occasionally (reboot, etc).
sqliteDBPath := filepath.Join(cfg.DataDir, "loop_sqlite.db")
if lnrpc.FileExists(sqliteDBPath) {
log.Infof("Found sqlite db at %v, skipping migration",
sqliteDBPath)
return false
}
// Now we'll check if the bolt db exists.
if !lnrpc.FileExists(filepath.Join(cfg.DataDir, "loop.db")) {
return false

@ -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),
@ -716,9 +561,9 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
}, nil
}
// GetLoopInTerms returns the terms that the server enforces for swaps.
// GetTerms 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")
@ -734,7 +579,7 @@ func (s *swapClientServer) GetLoopInTerms(ctx context.Context,
}, nil
}
// GetLoopInQuote returns a quote for a swap with the provided parameters.
// GetQuote returns a quote for a swap with the provided parameters.
func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
req *clientrpc.QuoteRequest) (*clientrpc.InQuoteResponse, error) {
@ -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,28 +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(
loopdb.NewTypedStore[sweepbatcher.Querier](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
@ -63,9 +49,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 {
@ -114,9 +98,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.

@ -142,7 +142,7 @@ func (m *MigratorManager) migrateLoopIns(ctx context.Context) error {
swapMap := make(map[lntypes.Hash]*LoopInContract)
updateMap := make(map[lntypes.Hash][]BatchInsertUpdateData)
// For each loop in, create a new loop in the toStore.
// For each loop in, create a new loop in in the toStore.
for _, loopIn := range loopIns {
swapMap[loopIn.Hash] = loopIn.Contract
@ -311,7 +311,6 @@ func (m *MigratorManager) checkLiquidityParams(ctx context.Context) error {
func equalizeLoopOut(fromLoopOut, toLoopOut *LoopOut) error {
if fromLoopOut.Contract.InitiationTime.Unix() !=
toLoopOut.Contract.InitiationTime.Unix() {
return fmt.Errorf("initiation time mismatch")
}
@ -319,7 +318,6 @@ func equalizeLoopOut(fromLoopOut, toLoopOut *LoopOut) error {
if fromLoopOut.Contract.SwapPublicationDeadline.Unix() !=
toLoopOut.Contract.SwapPublicationDeadline.Unix() {
return fmt.Errorf("swap publication deadline mismatch")
}
@ -339,7 +337,6 @@ func equalizeLoopOut(fromLoopOut, toLoopOut *LoopOut) error {
func equalizeLoopIns(fromLoopIn, toLoopIn *LoopIn) error {
if fromLoopIn.Contract.InitiationTime.Unix() !=
toLoopIn.Contract.InitiationTime.Unix() {
return fmt.Errorf("initiation time mismatch")
}
@ -401,6 +398,17 @@ func equalValues(src interface{}, dst interface{}) error {
return nil
}
func elementsMatch(src interface{}, dst interface{}) error {
mt := &mockTesting{}
require.ElementsMatch(mt, src, dst)
if mt.fail || mt.failNow {
return fmt.Errorf(mt.format, mt.args)
}
return nil
}
type mockTesting struct {
failNow bool
fail bool

@ -108,7 +108,7 @@ func newReplacerFile(parent fs.File, replaces map[string]string) (*replacerFile,
contentStr := string(content)
for from, to := range replaces {
contentStr = strings.ReplaceAll(contentStr, from, to)
contentStr = strings.Replace(contentStr, from, to, -1)
}
var buf bytes.Buffer

@ -115,7 +115,7 @@ func NewPostgresStore(cfg *PostgresConfig,
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
err = baseDB.FixFaultyTimestamps(ctx)
err = baseDB.FixFaultyTimestamps(ctx, parsePostgresTimeStamp)
if err != nil {
log.Errorf("Failed to fix faulty timestamps: %v", err)
return nil, err

@ -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

@ -14,12 +14,12 @@ import (
//
// Example output:
//
// map[string]interface{}{
// Hex("1234"): map[string]interface{}{
// "human-readable": Hex("102030"),
// Hex("1111"): Hex("5783492373"),
// },
// } .
// map[string]interface{}{
// Hex("1234"): map[string]interface{}{
// "human-readable": Hex("102030"),
// Hex("1111"): Hex("5783492373"),
// },
// } .
func DumpDB(tx *bbolt.Tx) error { // nolint: unused
return tx.ForEach(func(k []byte, bucket *bbolt.Bucket) error {
key := toString(k)

@ -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"
@ -18,13 +17,13 @@ import (
)
// FetchLoopOutSwaps returns all swaps currently in the store.
func (db *BaseDB) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut,
func (s *BaseDB) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut,
error) {
var loopOuts []*LoopOut
err := db.ExecTx(ctx, NewSqlReadOpts(), func(tx *sqlc.Queries) error {
swaps, err := tx.GetLoopOutSwaps(ctx)
err := s.ExecTx(ctx, NewSqlReadOpts(), func(*sqlc.Queries) error {
swaps, err := s.Queries.GetLoopOutSwaps(ctx)
if err != nil {
return err
}
@ -32,16 +31,13 @@ func (db *BaseDB) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut,
loopOuts = make([]*LoopOut, len(swaps))
for i, swap := range swaps {
updates, err := tx.GetSwapUpdates(
ctx, swap.SwapHash,
)
updates, err := s.Queries.GetSwapUpdates(ctx, swap.SwapHash)
if err != nil {
return err
}
loopOut, err := ConvertLoopOutRow(
db.network, sqlc.GetLoopOutSwapRow(swap),
updates,
loopOut, err := s.convertLoopOutRow(
sqlc.GetLoopOutSwapRow(swap), updates,
)
if err != nil {
return err
@ -60,24 +56,24 @@ func (db *BaseDB) FetchLoopOutSwaps(ctx context.Context) ([]*LoopOut,
}
// FetchLoopOutSwap returns the loop out swap with the given hash.
func (db *BaseDB) FetchLoopOutSwap(ctx context.Context,
func (s *BaseDB) FetchLoopOutSwap(ctx context.Context,
hash lntypes.Hash) (*LoopOut, error) {
var loopOut *LoopOut
err := db.ExecTx(ctx, NewSqlReadOpts(), func(tx *sqlc.Queries) error {
swap, err := tx.GetLoopOutSwap(ctx, hash[:])
err := s.ExecTx(ctx, NewSqlReadOpts(), func(*sqlc.Queries) error {
swap, err := s.Queries.GetLoopOutSwap(ctx, hash[:])
if err != nil {
return err
}
updates, err := tx.GetSwapUpdates(ctx, swap.SwapHash)
updates, err := s.Queries.GetSwapUpdates(ctx, swap.SwapHash)
if err != nil {
return err
}
loopOut, err = ConvertLoopOutRow(
db.network, swap, updates,
loopOut, err = s.convertLoopOutRow(
swap, updates,
)
if err != nil {
return err
@ -93,11 +89,11 @@ func (db *BaseDB) FetchLoopOutSwap(ctx context.Context,
}
// CreateLoopOut adds an initiated swap to the store.
func (db *BaseDB) CreateLoopOut(ctx context.Context, hash lntypes.Hash,
func (s *BaseDB) CreateLoopOut(ctx context.Context, hash lntypes.Hash,
swap *LoopOutContract) error {
writeOpts := NewSqlWriteOpts()
return db.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
writeOpts := &SqliteTxOptions{}
return s.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
insertArgs := loopToInsertArgs(
hash, &swap.SwapContract,
)
@ -131,14 +127,12 @@ func (db *BaseDB) CreateLoopOut(ctx context.Context, hash lntypes.Hash,
}
// BatchCreateLoopOut adds multiple initiated swaps to the store.
func (db *BaseDB) BatchCreateLoopOut(ctx context.Context,
func (s *BaseDB) BatchCreateLoopOut(ctx context.Context,
swaps map[lntypes.Hash]*LoopOutContract) error {
writeOpts := NewSqlWriteOpts()
return db.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
writeOpts := &SqliteTxOptions{}
return s.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
for swapHash, swap := range swaps {
swap := swap
insertArgs := loopToInsertArgs(
swapHash, &swap.SwapContract,
)
@ -174,20 +168,20 @@ func (db *BaseDB) BatchCreateLoopOut(ctx context.Context,
// 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.
func (db *BaseDB) UpdateLoopOut(ctx context.Context, hash lntypes.Hash,
func (s *BaseDB) UpdateLoopOut(ctx context.Context, hash lntypes.Hash,
time time.Time, state SwapStateData) error {
return db.updateLoop(ctx, hash, time, state)
return s.updateLoop(ctx, hash, time, state)
}
// FetchLoopInSwaps returns all swaps currently in the store.
func (db *BaseDB) FetchLoopInSwaps(ctx context.Context) (
func (s *BaseDB) FetchLoopInSwaps(ctx context.Context) (
[]*LoopIn, error) {
var loopIns []*LoopIn
err := db.ExecTx(ctx, NewSqlReadOpts(), func(tx *sqlc.Queries) error {
swaps, err := tx.GetLoopInSwaps(ctx)
err := s.ExecTx(ctx, NewSqlReadOpts(), func(*sqlc.Queries) error {
swaps, err := s.Queries.GetLoopInSwaps(ctx)
if err != nil {
return err
}
@ -195,12 +189,12 @@ func (db *BaseDB) FetchLoopInSwaps(ctx context.Context) (
loopIns = make([]*LoopIn, len(swaps))
for i, swap := range swaps {
updates, err := tx.GetSwapUpdates(ctx, swap.SwapHash)
updates, err := s.Queries.GetSwapUpdates(ctx, swap.SwapHash)
if err != nil {
return err
}
loopIn, err := db.convertLoopInRow(
loopIn, err := s.convertLoopInRow(
swap, updates,
)
if err != nil {
@ -220,11 +214,11 @@ func (db *BaseDB) FetchLoopInSwaps(ctx context.Context) (
}
// CreateLoopIn adds an initiated swap to the store.
func (db *BaseDB) CreateLoopIn(ctx context.Context, hash lntypes.Hash,
func (s *BaseDB) CreateLoopIn(ctx context.Context, hash lntypes.Hash,
swap *LoopInContract) error {
writeOpts := NewSqlWriteOpts()
return db.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
writeOpts := &SqliteTxOptions{}
return s.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
insertArgs := loopToInsertArgs(
hash, &swap.SwapContract,
)
@ -256,15 +250,13 @@ func (db *BaseDB) CreateLoopIn(ctx context.Context, hash lntypes.Hash,
})
}
// BatchCreateLoopIn adds multiple initiated swaps to the store.
func (db *BaseDB) BatchCreateLoopIn(ctx context.Context,
// BatchCreateLoopOut adds multiple initiated swaps to the store.
func (s *BaseDB) BatchCreateLoopIn(ctx context.Context,
swaps map[lntypes.Hash]*LoopInContract) error {
writeOpts := NewSqlWriteOpts()
return db.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
writeOpts := &SqliteTxOptions{}
return s.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
for swapHash, swap := range swaps {
swap := swap
insertArgs := loopToInsertArgs(
swapHash, &swap.SwapContract,
)
@ -301,10 +293,10 @@ func (db *BaseDB) BatchCreateLoopIn(ctx context.Context,
// 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.
func (db *BaseDB) UpdateLoopIn(ctx context.Context, hash lntypes.Hash,
func (s *BaseDB) UpdateLoopIn(ctx context.Context, hash lntypes.Hash,
time time.Time, state SwapStateData) error {
return db.updateLoop(ctx, hash, time, state)
return s.updateLoop(ctx, hash, time, state)
}
// PutLiquidityParams writes the serialized `manager.Parameters` bytes
@ -312,10 +304,10 @@ func (db *BaseDB) UpdateLoopIn(ctx context.Context, hash lntypes.Hash,
//
// NOTE: it's the caller's responsibility to encode the param. Atm,
// it's encoding using the proto package's `Marshal` method.
func (db *BaseDB) PutLiquidityParams(ctx context.Context,
func (s *BaseDB) PutLiquidityParams(ctx context.Context,
params []byte) error {
err := db.Queries.UpsertLiquidityParams(ctx, params)
err := s.Queries.UpsertLiquidityParams(ctx, params)
if err != nil {
return err
}
@ -328,11 +320,11 @@ func (db *BaseDB) PutLiquidityParams(ctx context.Context,
//
// NOTE: it's the caller's responsibility to decode the param. Atm,
// it's decoding using the proto package's `Unmarshal` method.
func (db *BaseDB) FetchLiquidityParams(ctx context.Context) ([]byte,
func (s *BaseDB) FetchLiquidityParams(ctx context.Context) ([]byte,
error) {
var params []byte
params, err := db.Queries.FetchLiquidityParams(ctx)
params, err := s.Queries.FetchLiquidityParams(ctx)
if errors.Is(err, sql.ErrNoRows) {
return params, nil
} else if err != nil {
@ -348,11 +340,11 @@ var _ SwapStore = (*BaseDB)(nil)
// updateLoop updates the swap with the given hash by inserting a new update
// in the swap_updates table.
func (db *BaseDB) updateLoop(ctx context.Context, hash lntypes.Hash,
func (s *BaseDB) updateLoop(ctx context.Context, hash lntypes.Hash,
time time.Time, state SwapStateData) error {
writeOpts := NewSqlWriteOpts()
return db.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
writeOpts := &SqliteTxOptions{}
return s.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
updateParams := sqlc.InsertSwapUpdateParams{
SwapHash: hash[:],
UpdateTimestamp: time.UTC(),
@ -376,11 +368,11 @@ func (db *BaseDB) updateLoop(ctx context.Context, hash lntypes.Hash,
}
// BatchInsertUpdate inserts multiple swap updates to the store.
func (db *BaseDB) BatchInsertUpdate(ctx context.Context,
func (s *BaseDB) BatchInsertUpdate(ctx context.Context,
updateData map[lntypes.Hash][]BatchInsertUpdateData) error {
writeOpts := NewSqlWriteOpts()
return db.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
writeOpts := &SqliteTxOptions{}
return s.ExecTx(ctx, writeOpts, func(tx *sqlc.Queries) error {
for swapHash, updates := range updateData {
for _, update := range updates {
updateParams := sqlc.InsertSwapUpdateParams{
@ -407,61 +399,6 @@ func (db *BaseDB) BatchInsertUpdate(ctx context.Context,
})
}
// BatchUpdateLoopOutSwapCosts updates the swap costs for a batch of loop out
// swaps.
func (db *BaseDB) BatchUpdateLoopOutSwapCosts(ctx context.Context,
costs map[lntypes.Hash]SwapCost) error {
writeOpts := NewSqlWriteOpts()
return db.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 (db *BaseDB) HasMigration(ctx context.Context, migrationID string) (
bool, error) {
migration, err := db.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 (db *BaseDB) SetMigration(ctx context.Context, migrationID string) error {
return db.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,
@ -485,11 +422,9 @@ func loopToInsertArgs(hash lntypes.Hash,
// needed to insert it into the database.
func loopOutToInsertArgs(hash lntypes.Hash,
loopOut *LoopOutContract) sqlc.InsertLoopOutParams {
return sqlc.InsertLoopOutParams{
SwapHash: hash[:],
DestAddress: loopOut.DestAddr.String(),
SingleSweep: loopOut.IsExternalAddr,
SwapInvoice: loopOut.SwapInvoice,
MaxSwapRoutingFee: int64(loopOut.MaxSwapRoutingFee),
SweepConfTarget: loopOut.SweepConfTarget,
@ -498,7 +433,6 @@ func loopOutToInsertArgs(hash lntypes.Hash,
PrepayInvoice: loopOut.PrepayInvoice,
MaxPrepayRoutingFee: int64(loopOut.MaxPrepayRoutingFee),
PublicationDeadline: loopOut.SwapPublicationDeadline.UTC(),
PaymentTimeout: int32(loopOut.PaymentTimeout.Seconds()),
}
}
@ -524,7 +458,6 @@ func loopInToInsertArgs(hash lntypes.Hash,
// and converts them to the arguments needed to insert them into the database.
func swapToHtlcKeysInsertArgs(hash lntypes.Hash,
swap *SwapContract) sqlc.InsertHtlcKeysParams {
return sqlc.InsertHtlcKeysParams{
SwapHash: hash[:],
SenderScriptPubkey: swap.HtlcKeys.SenderScriptKey[:],
@ -540,9 +473,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 +492,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 +517,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 +524,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 +531,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
}
@ -627,7 +556,7 @@ func ConvertLoopOutRow(network *chaincfg.Params, row sqlc.GetLoopOutSwapRow,
// convertLoopInRow converts a database row containing a loop in swap to a
// LoopIn struct.
func (db *BaseDB) convertLoopInRow(row sqlc.GetLoopInSwapsRow,
func (s *BaseDB) convertLoopInRow(row sqlc.GetLoopInSwapsRow,
updates []sqlc.SwapUpdate) (*LoopIn, error) {
htlcKeys, err := fetchHtlcKeys(
@ -725,9 +654,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,13 +13,13 @@ 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"
)
const (
testLabel = "test label"
var (
testTime1 = time.Date(2018, time.January, 9, 14, 54, 32, 3, time.UTC)
testTime2 = time.Date(2018, time.January, 9, 15, 02, 03, 5, time.UTC)
)
// TestSqliteLoopOutStore tests all the basic functionality of the current
@ -61,7 +61,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) {
@ -76,7 +75,7 @@ func TestSqliteLoopOutStore(t *testing.T) {
})
labelledSwap := unrestrictedSwap
labelledSwap.Label = testLabel
labelledSwap.Label = "test label"
t.Run("labelled swap", func(t *testing.T) {
testSqliteLoopOutStore(t, &labelledSwap)
})
@ -122,8 +121,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
@ -209,7 +206,7 @@ func TestSQLliteLoopInStore(t *testing.T) {
})
labelledSwap := pendingSwap
labelledSwap.Label = testLabel
labelledSwap.Label = "test label"
t.Run("loop in with label", func(t *testing.T) {
testSqliteLoopInStore(t, labelledSwap)
})
@ -313,12 +310,12 @@ func TestSqliteLiquidityParams(t *testing.T) {
// convert between the :one and :many types from sqlc.
func TestSqliteTypeConversion(t *testing.T) {
loopOutSwapRow := sqlc.GetLoopOutSwapRow{}
err := randomStruct(&loopOutSwapRow)
require.NoError(t, err)
randomStruct(&loopOutSwapRow)
require.NotNil(t, loopOutSwapRow.DestAddress)
loopOutSwapsRow := sqlc.GetLoopOutSwapsRow(loopOutSwapRow)
require.EqualValues(t, loopOutSwapRow, loopOutSwapsRow)
}
// TestIssue615 tests that on faulty timestamps, the database will be fixed.
@ -326,24 +323,13 @@ 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)
// Create a faulty loopout swap.
destAddr := test.GetDestAddr(t, 0)
// Corresponds to 55563-06-27 02:09:24 +0000 UTC.
faultyTime := time.Unix(1691247002964, 0)
t.Log(faultyTime.Unix())
faultyTime, err := parseSqliteTimeStamp("55563-06-27 02:09:24 +0000 UTC")
require.NoError(t, err)
unrestrictedSwap := LoopOutContract{
SwapContract: SwapContract{
@ -369,14 +355,14 @@ func TestIssue615(t *testing.T) {
MaxPrepayRoutingFee: 40,
PrepayInvoice: "prepayinvoice",
DestAddr: destAddr,
SwapInvoice: invoice,
SwapInvoice: "swapinvoice",
MaxSwapRoutingFee: 30,
SweepConfTarget: 2,
HtlcConfirmations: 2,
SwapPublicationDeadline: faultyTime,
}
err := sqlDB.CreateLoopOut(ctxb, testPreimage.Hash(), &unrestrictedSwap)
err = sqlDB.CreateLoopOut(ctxb, testPreimage.Hash(), &unrestrictedSwap)
require.NoError(t, err)
// This should fail because of the faulty timestamp.
@ -389,146 +375,17 @@ func TestIssue615(t *testing.T) {
require.NoError(t, err)
}
// Fix the faulty timestamp.
err = sqlDB.FixFaultyTimestamps(ctxb)
require.NoError(t, err)
_, err = sqlDB.GetLoopOutSwaps(ctxb)
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,
},
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,
},
},
)
require.NoError(t, err)
err = store.UpdateLoopOut(
ctxb, hash2, testTime,
SwapStateData{
State: StateSuccess,
Cost: SwapCost{
Server: 4,
Onchain: 5,
Offchain: 6,
},
},
)
require.NoError(t, err)
updateMap := map[lntypes.Hash]SwapCost{
hash1: {
Server: 2,
Onchain: 3,
Offchain: 4,
},
hash2: {
Server: 6,
Onchain: 7,
Offchain: 8,
},
parseFunc := parseSqliteTimeStamp
if testDBType == "postgres" {
parseFunc = parsePostgresTimeStamp
}
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")
// Fix the faulty timestamp.
err = sqlDB.FixFaultyTimestamps(ctxb, parseFunc)
require.NoError(t, err)
require.False(t, hasMigration)
require.NoError(t, sqlDB.SetMigration(ctxb, "test"))
hasMigration, err = sqlDB.HasMigration(ctxb, "test")
_, err = sqlDB.GetLoopOutSwaps(ctxb)
require.NoError(t, err)
require.True(t, hasMigration)
}
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)

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

Loading…
Cancel
Save