mirror of https://github.com/lightninglabs/loop
Compare commits
No commits in common. 'master' and 'v0.26.1-beta' have entirely different histories.
master
...
v0.26.1-be
@ -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'
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
@ -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,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
|
|
||||||
}
|
|
@ -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])
|
|
||||||
}
|
|
@ -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,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,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,2 +0,0 @@
|
|||||||
DROP TABLE migration_tracker;
|
|
||||||
|
|
@ -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,108 +0,0 @@
|
|||||||
-- name: GetUnconfirmedBatches :many
|
|
||||||
SELECT
|
|
||||||
*
|
|
||||||
FROM
|
|
||||||
sweep_batches
|
|
||||||
WHERE
|
|
||||||
confirmed = FALSE;
|
|
||||||
|
|
||||||
-- name: InsertBatch :one
|
|
||||||
INSERT INTO sweep_batches (
|
|
||||||
confirmed,
|
|
||||||
batch_tx_id,
|
|
||||||
batch_pk_script,
|
|
||||||
last_rbf_height,
|
|
||||||
last_rbf_sat_per_kw,
|
|
||||||
max_timeout_distance
|
|
||||||
) VALUES (
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
$6
|
|
||||||
) RETURNING id;
|
|
||||||
|
|
||||||
-- name: DropBatch :exec
|
|
||||||
DELETE FROM sweep_batches WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: UpdateBatch :exec
|
|
||||||
UPDATE sweep_batches SET
|
|
||||||
confirmed = $2,
|
|
||||||
batch_tx_id = $3,
|
|
||||||
batch_pk_script = $4,
|
|
||||||
last_rbf_height = $5,
|
|
||||||
last_rbf_sat_per_kw = $6
|
|
||||||
WHERE id = $1;
|
|
||||||
|
|
||||||
-- name: ConfirmBatch :exec
|
|
||||||
UPDATE
|
|
||||||
sweep_batches
|
|
||||||
SET
|
|
||||||
confirmed = TRUE
|
|
||||||
WHERE
|
|
||||||
id = $1;
|
|
||||||
|
|
||||||
-- name: UpsertSweep :exec
|
|
||||||
INSERT INTO sweeps (
|
|
||||||
swap_hash,
|
|
||||||
batch_id,
|
|
||||||
outpoint_txid,
|
|
||||||
outpoint_index,
|
|
||||||
amt,
|
|
||||||
completed
|
|
||||||
) VALUES (
|
|
||||||
$1,
|
|
||||||
$2,
|
|
||||||
$3,
|
|
||||||
$4,
|
|
||||||
$5,
|
|
||||||
$6
|
|
||||||
) ON CONFLICT (swap_hash) DO UPDATE SET
|
|
||||||
batch_id = $2,
|
|
||||||
outpoint_txid = $3,
|
|
||||||
outpoint_index = $4,
|
|
||||||
amt = $5,
|
|
||||||
completed = $6;
|
|
||||||
|
|
||||||
-- name: GetParentBatch :one
|
|
||||||
SELECT
|
|
||||||
sweep_batches.*
|
|
||||||
FROM
|
|
||||||
sweep_batches
|
|
||||||
JOIN
|
|
||||||
sweeps ON sweep_batches.id = sweeps.batch_id
|
|
||||||
WHERE
|
|
||||||
sweeps.swap_hash = $1
|
|
||||||
AND
|
|
||||||
sweeps.completed = TRUE
|
|
||||||
AND
|
|
||||||
sweep_batches.confirmed = TRUE;
|
|
||||||
|
|
||||||
-- name: GetBatchSweptAmount :one
|
|
||||||
SELECT
|
|
||||||
SUM(amt) AS total
|
|
||||||
FROM
|
|
||||||
sweeps
|
|
||||||
WHERE
|
|
||||||
batch_id = $1
|
|
||||||
AND
|
|
||||||
completed = TRUE;
|
|
||||||
|
|
||||||
-- name: GetBatchSweeps :many
|
|
||||||
SELECT
|
|
||||||
*
|
|
||||||
FROM
|
|
||||||
sweeps
|
|
||||||
WHERE
|
|
||||||
batch_id = $1
|
|
||||||
ORDER BY
|
|
||||||
id ASC;
|
|
||||||
|
|
||||||
-- name: GetSweepStatus :one
|
|
||||||
SELECT
|
|
||||||
COALESCE(s.completed, f.false_value) AS completed
|
|
||||||
FROM
|
|
||||||
(SELECT false AS false_value) AS f
|
|
||||||
LEFT JOIN
|
|
||||||
sweeps s ON s.swap_hash = $1;
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue