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
|
||||
}
|
||||
|
||||
// NewFixedSizeSlice 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,635 +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"
|
||||
"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,
|
||||
&swapserverrpc.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,
|
||||
&swapserverrpc.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,
|
||||
&swapserverrpc.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,
|
||||
&swapserverrpc.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,
|
||||
&swapserverrpc.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, &swapserverrpc.CancelInstantSwapRequest{
|
||||
SwapHash: f.InstantOut.SwapHash[:],
|
||||
},
|
||||
)
|
||||
if cancelErr != nil {
|
||||
// We'll log the error but not return it as we want to return the
|
||||
// original error.
|
||||
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"
|
||||
"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() swapserverrpc.InstantOutProtocolVersion {
|
||||
return swapserverrpc.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 swapserverrpc.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"
|
||||
"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, &swapserverrpc.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"
|
||||
"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 := &swapserverrpc.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"
|
||||
"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 swapserverrpc.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,439 +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,
|
||||
network: network,
|
||||
}
|
||||
}
|
||||
|
||||
// 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,292 +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
|
||||
`
|
||||
|
||||
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)
|
||||
);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue