Browse Source

multi: loop in swap

pull/34/head
Joost Jager 2 years ago
parent
commit
3e960b8b54
No known key found for this signature in database GPG Key ID: A61B9D4C393C59C7
28 changed files with 2377 additions and 274 deletions
  1. +115
    -9
      client.go
  2. +102
    -0
      cmd/loop/loopin.go
  3. +3
    -3
      cmd/loop/loopout.go
  4. +36
    -17
      cmd/loop/main.go
  5. +13
    -8
      cmd/loop/terms.go
  6. +16
    -2
      cmd/loopd/daemon.go
  7. +82
    -3
      cmd/loopd/swapclient_server.go
  8. +94
    -0
      interface.go
  9. +3
    -12
      lndclient/chainnotifier_client.go
  10. +15
    -9
      lndclient/invoices_client.go
  11. +28
    -16
      lndclient/lightning_client.go
  12. +585
    -0
      loopin.go
  13. +370
    -0
      loopin_test.go
  14. +62
    -0
      loopin_testcontext_test.go
  15. +1
    -1
      loopout.go
  16. +279
    -77
      looprpc/client.pb.go
  17. +57
    -5
      looprpc/client.proto
  18. +5
    -8
      looprpc/client.swagger.json
  19. +323
    -38
      looprpc/server.pb.go
  20. +27
    -0
      looprpc/server.proto
  21. +38
    -0
      server_mock_test.go
  22. +67
    -0
      swap_server_client.go
  23. +5
    -12
      test/context.go
  24. +3
    -3
      test/invoices_mock.go
  25. +13
    -43
      test/lightning_client_mock.go
  26. +2
    -3
      test/lnd_services_mock.go
  27. +30
    -4
      test/testutils.go
  28. +3
    -1
      testcontext_test.go

+ 115
- 9
client.go View File

@ -118,6 +118,11 @@ func (s *Client) FetchLoopOutSwaps() ([]*loopdb.LoopOut, error) {
return s.Store.FetchLoopOutSwaps()
}
// FetchLoopInSwaps returns a list of all swaps currently in the database.
func (s *Client) FetchLoopInSwaps() ([]*loopdb.LoopIn, error) {
return s.Store.FetchLoopInSwaps()
}
// Run is a blocking call that executes all swaps. Any pending swaps are
// restored from persistent storage and resumed. Subsequent updates will be
// sent through the passed in statusChan. The function can be terminated by
@ -144,7 +149,12 @@ func (s *Client) Run(ctx context.Context,
// Query store before starting event loop to prevent new swaps from
// being treated as swaps that need to be resumed.
pendingSwaps, err := s.Store.FetchLoopOutSwaps()
pendingLoopOutSwaps, err := s.Store.FetchLoopOutSwaps()
if err != nil {
return err
}
pendingLoopInSwaps, err := s.Store.FetchLoopInSwaps()
if err != nil {
return err
}
@ -154,7 +164,7 @@ func (s *Client) Run(ctx context.Context,
go func() {
defer s.wg.Done()
s.resumeSwaps(mainCtx, pendingSwaps)
s.resumeSwaps(mainCtx, pendingLoopOutSwaps, pendingLoopInSwaps)
// Signal that new requests can be accepted. Otherwise the new
// swap could already have been added to the store and read in
@ -194,19 +204,33 @@ func (s *Client) Run(ctx context.Context,
// resumeSwaps restarts all pending swaps from the provided list.
func (s *Client) resumeSwaps(ctx context.Context,
swaps []*loopdb.LoopOut) {
loopOutSwaps []*loopdb.LoopOut, loopInSwaps []*loopdb.LoopIn) {
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
}
for _, pend := range swaps {
for _, pend := range loopOutSwaps {
if pend.State().Type() != loopdb.StateTypePending {
continue
}
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
}
swap, err := resumeLoopOutSwap(ctx, swapCfg, pend)
if err != nil {
logger.Errorf("resuming swap: %v", err)
logger.Errorf("resuming loop out swap: %v", err)
continue
}
s.executor.initiateSwap(ctx, swap)
}
for _, pend := range loopInSwaps {
if pend.State().Type() != loopdb.StateTypePending {
continue
}
swap, err := resumeLoopInSwap(ctx, swapCfg, pend)
if err != nil {
logger.Errorf("resuming loop in swap: %v", err)
continue
}
@ -320,3 +344,85 @@ func (s *Client) waitForInitialized(ctx context.Context) error {
return nil
}
// LoopIn initiates a loop in swap.
func (s *Client) LoopIn(globalCtx context.Context,
request *LoopInRequest) (*lntypes.Hash, error) {
logger.Infof("Loop in %v (channel: %v)",
request.Amount,
request.LoopInChannel,
)
if err := s.waitForInitialized(globalCtx); err != nil {
return nil, err
}
// Create a new swap object for this swap.
initiationHeight := s.executor.height()
swapCfg := swapConfig{
lnd: s.lndServices,
store: s.Store,
server: s.Server,
}
swap, err := newLoopInSwap(
globalCtx, &swapCfg, initiationHeight, request,
)
if err != nil {
return nil, err
}
// Post swap to the main loop.
s.executor.initiateSwap(globalCtx, swap)
// Return hash so that the caller can identify this swap in the updates
// stream.
return &swap.hash, nil
}
// LoopInQuote takes an amount and returns a break down of estimated
// costs for the client. Both the swap server and the on-chain fee estimator are
// queried to get to build the quote response.
func (s *Client) LoopInQuote(ctx context.Context,
request *LoopInQuoteRequest) (*LoopInQuote, error) {
// Retrieve current server terms to calculate swap fee.
terms, err := s.Server.GetLoopInTerms(ctx)
if err != nil {
return nil, err
}
// Check amount limits.
if request.Amount < terms.MinSwapAmount {
return nil, ErrSwapAmountTooLow
}
if request.Amount > terms.MaxSwapAmount {
return nil, ErrSwapAmountTooHigh
}
// Calculate swap fee.
swapFee := terms.SwapFeeBase +
request.Amount*btcutil.Amount(terms.SwapFeeRate)/
btcutil.Amount(swap.FeeRateTotalParts)
// Get estimate for miner fee.
minerFee, err := s.lndServices.Client.EstimateFeeToP2WSH(
ctx, request.Amount, request.HtlcConfTarget,
)
if err != nil {
return nil, err
}
return &LoopInQuote{
SwapFee: swapFee,
MinerFee: minerFee,
}, nil
}
// LoopInTerms returns the terms on which the server executes swaps.
func (s *Client) LoopInTerms(ctx context.Context) (
*LoopInTerms, error) {
return s.Server.GetLoopInTerms(ctx)
}

+ 102
- 0
cmd/loop/loopin.go View File

@ -0,0 +1,102 @@
package main
import (
"context"
"fmt"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
)
var loopInCommand = cli.Command{
Name: "in",
Usage: "perform an on-chain to off-chain swap (loop in)",
ArgsUsage: "amt",
Description: `
Send the amount in satoshis specified by the amt argument off-chain.`,
Flags: []cli.Flag{
cli.Uint64Flag{
Name: "channel",
Usage: "the 8-byte compact channel ID of the channel to loop in",
},
cli.Uint64Flag{
Name: "amt",
Usage: "the amount in satoshis to loop out",
},
},
Action: loopIn,
}
func loopIn(ctx *cli.Context) error {
args := ctx.Args()
var amtStr string
switch {
case ctx.IsSet("amt"):
amtStr = ctx.String("amt")
case ctx.NArg() > 0:
amtStr = args[0]
args = args.Tail()
default:
// Show command help if no arguments and flags were provided.
cli.ShowCommandHelp(ctx, "in")
return nil
}
amt, err := parseAmt(amtStr)
if err != nil {
return err
}
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
quote, err := client.GetLoopInQuote(
context.Background(),
&looprpc.QuoteRequest{
Amt: int64(amt),
},
)
if err != nil {
return err
}
limits := getInLimits(amt, quote)
if err := displayLimits(amt, limits); err != nil {
return err
}
var loopInChannel uint64
if ctx.IsSet("channel") {
loopInChannel = ctx.Uint64("channel")
}
resp, err := client.LoopIn(context.Background(), &looprpc.LoopInRequest{
Amt: int64(amt),
MaxMinerFee: int64(limits.maxMinerFee),
MaxSwapFee: int64(limits.maxSwapFee),
LoopInChannel: loopInChannel,
})
if err != nil {
return err
}
fmt.Printf("Swap initiated with id: %v\n", resp.Id[:8])
fmt.Printf("Run swapcli without a command to monitor progress.\n")
return nil
}
func getInLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits {
return &limits{
// Apply a multiplier to the estimated miner fee, to not get
// the swap canceled because fees increased in the mean time.
maxMinerFee: btcutil.Amount(quote.MinerFee) * 3,
maxSwapFee: btcutil.Amount(quote.SwapFee),
}
}

+ 3
- 3
cmd/loop/loopout.go View File

@ -97,10 +97,10 @@ func loopOut(ctx *cli.Context) error {
Amt: int64(amt),
Dest: destAddr,
MaxMinerFee: int64(limits.maxMinerFee),
MaxPrepayAmt: int64(limits.maxPrepayAmt),
MaxPrepayAmt: int64(*limits.maxPrepayAmt),
MaxSwapFee: int64(limits.maxSwapFee),
MaxPrepayRoutingFee: int64(limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(limits.maxSwapRoutingFee),
MaxPrepayRoutingFee: int64(*limits.maxPrepayRoutingFee),
MaxSwapRoutingFee: int64(*limits.maxSwapRoutingFee),
LoopOutChannel: unchargeChannel,
})
if err != nil {

+ 36
- 17
cmd/loop/main.go View File

@ -62,7 +62,8 @@ func main() {
},
}
app.Commands = []cli.Command{
loopOutCommand, termsCommand, monitorCommand, quoteCommand,
loopOutCommand, loopInCommand, termsCommand,
monitorCommand, quoteCommand,
}
err := app.Run(os.Args)
@ -88,32 +89,41 @@ func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount {
}
type limits struct {
maxSwapRoutingFee btcutil.Amount
maxPrepayRoutingFee btcutil.Amount
maxSwapRoutingFee *btcutil.Amount
maxPrepayRoutingFee *btcutil.Amount
maxMinerFee btcutil.Amount
maxSwapFee btcutil.Amount
maxPrepayAmt btcutil.Amount
maxPrepayAmt *btcutil.Amount
}
func getLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits {
maxSwapRoutingFee := getMaxRoutingFee(btcutil.Amount(amt))
maxPrepayRoutingFee := getMaxRoutingFee(btcutil.Amount(
quote.PrepayAmt,
))
maxPrepayAmt := btcutil.Amount(quote.PrepayAmt)
return &limits{
maxSwapRoutingFee: getMaxRoutingFee(btcutil.Amount(amt)),
maxPrepayRoutingFee: getMaxRoutingFee(btcutil.Amount(
quote.PrepayAmt,
)),
maxSwapRoutingFee: &maxSwapRoutingFee,
maxPrepayRoutingFee: &maxPrepayRoutingFee,
// Apply a multiplier to the estimated miner fee, to not get
// the swap canceled because fees increased in the mean time.
maxMinerFee: btcutil.Amount(quote.MinerFee) * 3,
maxSwapFee: btcutil.Amount(quote.SwapFee),
maxPrepayAmt: btcutil.Amount(quote.PrepayAmt),
maxPrepayAmt: &maxPrepayAmt,
}
}
func displayLimits(amt btcutil.Amount, l *limits) error {
totalSuccessMax := l.maxSwapRoutingFee + l.maxPrepayRoutingFee +
l.maxMinerFee + l.maxSwapFee
totalSuccessMax := l.maxMinerFee + l.maxSwapFee
if l.maxSwapRoutingFee != nil {
totalSuccessMax += *l.maxSwapRoutingFee
}
if l.maxPrepayRoutingFee != nil {
totalSuccessMax += *l.maxPrepayRoutingFee
}
fmt.Printf("Max swap fees for %d loop out: %d\n",
btcutil.Amount(amt), totalSuccessMax,
@ -130,13 +140,22 @@ func displayLimits(amt btcutil.Amount, l *limits) error {
case "x":
fmt.Println()
fmt.Printf("Max on-chain fee: %d\n", l.maxMinerFee)
fmt.Printf("Max off-chain swap routing fee: %d\n",
l.maxSwapRoutingFee)
fmt.Printf("Max off-chain prepay routing fee: %d\n",
l.maxPrepayRoutingFee)
if l.maxSwapRoutingFee != nil {
fmt.Printf("Max off-chain swap routing fee: %d\n",
*l.maxSwapRoutingFee)
}
if l.maxPrepayRoutingFee != nil {
fmt.Printf("Max off-chain prepay routing fee: %d\n",
*l.maxPrepayRoutingFee)
}
fmt.Printf("Max swap fee: %d\n", l.maxSwapFee)
fmt.Printf("Max no show penalty: %d\n",
l.maxPrepayAmt)
if l.maxPrepayAmt != nil {
fmt.Printf("Max no show penalty: %d\n",
*l.maxPrepayAmt)
}
fmt.Printf("CONTINUE SWAP? (y/n): ")
fmt.Scanln(&answer)

+ 13
- 8
cmd/loop/terms.go View File

@ -4,11 +4,11 @@ import (
"context"
"fmt"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/swap"
"github.com/urfave/cli"
"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
"github.com/lightninglabs/loop/swap"
)
var termsCommand = cli.Command{
@ -25,14 +25,13 @@ func terms(ctx *cli.Context) error {
defer cleanup()
req := &looprpc.TermsRequest{}
terms, err := client.LoopOutTerms(context.Background(), req)
loopOutTerms, err := client.LoopOutTerms(context.Background(), req)
if err != nil {
return err
}
fmt.Printf("Amount: %d - %d\n",
btcutil.Amount(terms.MinSwapAmount),
btcutil.Amount(terms.MaxSwapAmount),
loopInTerms, err := client.GetLoopInTerms(
context.Background(), &looprpc.TermsRequest{},
)
if err != nil {
return err
@ -54,7 +53,13 @@ func terms(ctx *cli.Context) error {
fmt.Println("Loop Out")
fmt.Println("--------")
printTerms(terms)
printTerms(loopOutTerms)
fmt.Println()
fmt.Println("Loop In")
fmt.Println("------")
printTerms(loopInTerms)
return nil
}

+ 16
- 2
cmd/loopd/daemon.go View File

@ -43,11 +43,11 @@ func daemon(config *config) error {
// Before starting the client, build an in-memory view of all swaps.
// This view is used to update newly connected clients with the most
// recent swaps.
storedSwaps, err := swapClient.FetchLoopOutSwaps()
loopOutSwaps, err := swapClient.FetchLoopOutSwaps()
if err != nil {
return err
}
for _, swap := range storedSwaps {
for _, swap := range loopOutSwaps {
swaps[swap.Hash] = loop.SwapInfo{
SwapType: loop.TypeOut,
SwapContract: swap.Contract.SwapContract,
@ -57,6 +57,20 @@ func daemon(config *config) error {
}
}
loopInSwaps, err := swapClient.FetchLoopInSwaps()
if err != nil {
return err
}
for _, swap := range loopInSwaps {
swaps[swap.Hash] = loop.SwapInfo{
SwapType: loop.TypeIn,
SwapContract: swap.Contract.SwapContract,
State: swap.State(),
SwapHash: swap.Hash,
LastUpdate: swap.LastUpdateTime(),
}
}
// Instantiate the loopd gRPC server.
server := swapClientServer{
impl: swapClient,

+ 82
- 3
cmd/loopd/swapclient_server.go View File

@ -2,6 +2,7 @@ package main
import (
"context"
"errors"
"fmt"
"sort"
@ -32,7 +33,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context,
in *looprpc.LoopOutRequest) (
*looprpc.SwapResponse, error) {
logger.Infof("LoopOut request received")
logger.Infof("Loop out request received")
var sweepAddr btcutil.Address
if in.Dest == "" {
@ -85,6 +86,8 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
state = looprpc.SwapState_INITIATED
case loopdb.StatePreimageRevealed:
state = looprpc.SwapState_PREIMAGE_REVEALED
case loopdb.StateHtlcPublished:
state = looprpc.SwapState_HTLC_PUBLISHED
case loopdb.StateSuccess:
state = looprpc.SwapState_SUCCESS
default:
@ -105,6 +108,16 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
return nil, err
}
var swapType looprpc.SwapType
switch loopSwap.SwapType {
case loop.TypeIn:
swapType = looprpc.SwapType_LOOP_IN
case loop.TypeOut:
swapType = looprpc.SwapType_LOOP_OUT
default:
return nil, errors.New("unknown swap type")
}
return &looprpc.SwapStatus{
Amt: int64(loopSwap.AmountRequested),
Id: loopSwap.SwapHash.String(),
@ -112,7 +125,7 @@ func (s *swapClientServer) marshallSwap(loopSwap *loop.SwapInfo) (
InitiationTime: loopSwap.InitiationTime.UnixNano(),
LastUpdateTime: loopSwap.LastUpdate.UnixNano(),
HtlcAddress: address.EncodeAddress(),
Type: looprpcspan>.SwapType_LOOP_OUT,
Type: swapType,
}, nil
}
@ -214,7 +227,7 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest,
func (s *swapClientServer) LoopOutTerms(ctx context.Context,
req *looprpc.TermsRequest) (*looprpc.TermsResponse, error) {
logger.Infof("Terms request received")
logger.Infof("Loop out terms request received")
terms, err := s.impl.LoopOutTerms(ctx)
if err != nil {
@ -250,3 +263,69 @@ func (s *swapClientServer) LoopOutQuote(ctx context.Context,
SwapFee: int64(quote.SwapFee),
}, nil
}
// GetTerms returns the terms that the server enforces for swaps.
func (s *swapClientServer) GetLoopInTerms(ctx context.Context, req *looprpc.TermsRequest) (
*looprpc.TermsResponse, error) {
logger.Infof("Loop in terms request received")
terms, err := s.impl.LoopInTerms(ctx)
if err != nil {
logger.Errorf("Terms request: %v", err)
return nil, err
}
return &looprpc.TermsResponse{
MinSwapAmount: int64(terms.MinSwapAmount),
MaxSwapAmount: int64(terms.MaxSwapAmount),
SwapFeeBase: int64(terms.SwapFeeBase),
SwapFeeRate: int64(terms.SwapFeeRate),
CltvDelta: int32(terms.CltvDelta),
}, nil
}
// GetQuote returns a quote for a swap with the provided parameters.
func (s *swapClientServer) GetLoopInQuote(ctx context.Context,
req *looprpc.QuoteRequest) (*looprpc.QuoteResponse, error) {
logger.Infof("Loop in quote request received")
quote, err := s.impl.LoopInQuote(ctx, &loop.LoopInQuoteRequest{
Amount: btcutil.Amount(req.Amt),
HtlcConfTarget: defaultConfTarget,
})
if err != nil {
return nil, err
}
return &looprpc.QuoteResponse{
MinerFee: int64(quote.MinerFee),
SwapFee: int64(quote.SwapFee),
}, nil
}
func (s *swapClientServer) LoopIn(ctx context.Context,
in *looprpc.LoopInRequest) (
*looprpc.SwapResponse, error) {
logger.Infof("Loop in request received")
req := &loop.LoopInRequest{
Amount: btcutil.Amount(in.Amt),
MaxMinerFee: btcutil.Amount(in.MaxMinerFee),
MaxSwapFee: btcutil.Amount(in.MaxSwapFee),
HtlcConfTarget: defaultConfTarget,
}
if in.LoopInChannel != 0 {
req.LoopInChannel = &in.LoopInChannel
}
hash, err := s.impl.LoopIn(ctx, req)
if err != nil {
logger.Errorf("Loop in: %v", err)
return nil, err
}
return &looprpc.SwapResponse{
Id: hash.String(),
}, nil
}

+ 94
- 0
interface.go View File

@ -158,6 +158,90 @@ type LoopOutTerms struct {
SwapPaymentDest [33]byte
}
// LoopInRequest contains the required parameters for the swap.
type LoopInRequest struct {
// Amount specifies the requested swap amount in sat. This does not
// include the swap and miner fee.
Amount btcutil.Amount
// MaxSwapFee is the maximum we are willing to pay the server for the
// swap. This value is not disclosed in the swap initiation call, but if
// the server asks for a higher fee, we abort the swap. Typically this
// value is taken from the response of the UnchargeQuote call. It
// includes the prepay amount.
MaxSwapFee btcutil.Amount
// MaxMinerFee is the maximum in on-chain fees that we are willing to
// spent. If we publish the on-chain htlc and the fee estimate turns out
// higher than this value, we cancel the swap.
//
// MaxMinerFee is typically taken from the response of the UnchargeQuote
// call.
MaxMinerFee btcutil.Amount
// HtlcConfTarget specifies the targeted confirmation target for the
// client htlc tx.
HtlcConfTarget int32
// LoopInChannel optionally specifies the short channel id of the
// channel to charge.
LoopInChannel *uint64
}
// LoopInTerms are the server terms on which it executes charge swaps.
type LoopInTerms struct {
// SwapFeeBase is the fixed per-swap base fee.
SwapFeeBase btcutil.Amount
// SwapFeeRate is the variable fee in parts per million.
SwapFeeRate int64
// MinSwapAmount is the minimum amount that the server requires for a
// swap.
MinSwapAmount btcutil.Amount
// MaxSwapAmount is the maximum amount that the server accepts for a
// swap.
MaxSwapAmount btcutil.Amount
// Time lock delta relative to current block height that swap server
// will accept on the swap initiation call.
CltvDelta int32
}
// In contains status information for a loop in swap.
type In struct {
loopdb.LoopInContract
SwapInfoKit
// State where the swap is in.
State loopdb.SwapState
}
// LoopInQuoteRequest specifies the swap parameters for which a quote is
// requested.
type LoopInQuoteRequest struct {
// Amount specifies the requested swap amount in sat. This does not
// include the swap and miner fee.
Amount btcutil.Amount
// HtlcConfTarget specifies the targeted confirmation target for the
// client sweep tx.
HtlcConfTarget int32
}
// LoopInQuote contains estimates for the fees making up the total swap cost
// for the client.
type LoopInQuote struct {
// SwapFee is the fee that the swap server is charging for the swap.
SwapFee btcutil.Amount
// MinerFee is an estimate of the on-chain fee that needs to be paid to
// sweep the htlc.
MinerFee btcutil.Amount
}
// SwapInfoKit contains common swap info fields.
type SwapInfoKit struct {
// Hash is the sha256 hash of the preimage that unlocks the htlcs. It
@ -191,3 +275,13 @@ type SwapInfo struct {
loopdb.SwapContract
}
// LastUpdate returns the last update time of the swap
func (s *In) LastUpdate() time.Time {
return s.LastUpdateTime
}
// SwapHash returns the swap hash.
func (s *In) SwapHash() lntypes.Hash {
return s.Hash
}

+ 3
- 12
lndclient/chainnotifier_client.go View File

@ -11,8 +11,6 @@ import (
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ChainNotifierClient exposes base lightning functionality.
@ -101,9 +99,7 @@ func (s *chainNotifierClient) RegisterSpendNtfn(ctx context.Context,
for {
spendEvent, err := resp.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
errChan <- err
}
errChan <- err
return
}
@ -125,7 +121,6 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
txid *chainhash.Hash, pkScript []byte, numConfs, heightHint int32) (
chan *chainntnfs.TxConfirmation, chan error, error) {
// TODO: Height hint
var txidSlice []byte
if txid != nil {
txidSlice = txid[:]
@ -155,9 +150,7 @@ func (s *chainNotifierClient) RegisterConfirmationsNtfn(ctx context.Context,
var confEvent *chainrpc.ConfEvent
confEvent, err := confStream.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
errChan <- err
}
errChan <- err
return
}
@ -226,9 +219,7 @@ func (s *chainNotifierClient) RegisterBlockEpochNtfn(ctx context.Context) (
for {
epoch, err := blockEpochClient.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
blockErrorChan <- err
}
blockErrorChan <- err
return
}

+ 15
- 9
lndclient/invoices_client.go View File

@ -5,19 +5,18 @@ import (
"errors"
"sync"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// InvoicesClient exposes invoice functionality.
type InvoicesClient interface {
SubscribeSingleInvoice(ctx context.Context, hash lntypes.Hash) (
<-chan channeldb.ContractState, <-chan error, error)
<-chan InvoiceUpdate, <-chan error, error)
SettleInvoice(ctx context.Context, preimage lntypes.Preimage) error
@ -27,6 +26,12 @@ type InvoicesClient interface {
string, error)
}
// InvoiceUpdate contains a state update for an invoice.
type InvoiceUpdate struct {
State channeldb.ContractState
AmtPaid btcutil.Amount
}
type invoicesClient struct {
client invoicesrpc.InvoicesClient
wg sync.WaitGroup
@ -69,7 +74,7 @@ func (s *invoicesClient) CancelInvoice(ctx context.Context,
}
func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
hash lntypes.Hash) (<-chan channeldb.ContractState,
hash lntypes.Hash) (<-chan InvoiceUpdate,
<-chan error, error) {
invoiceStream, err := s.client.
@ -81,7 +86,7 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
return nil, nil, err
}
updateChan := make(chan channeldb.ContractState)
updateChan := make(chan InvoiceUpdate)
errChan := make(chan error, 1)
// Invoice updates goroutine.
@ -91,9 +96,7 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
for {
invoice, err := invoiceStream.Recv()
if err != nil {
if status.Code(err) != codes.Canceled {
errChan <- err
}
errChan <- err
return
}
@ -104,7 +107,10 @@ func (s *invoicesClient) SubscribeSingleInvoice(ctx context.Context,
}
select {
case updateChan <- state:
case updateChan <- InvoiceUpdate{
State: state,
AmtPaid: btcutil.Amount(invoice.AmtPaidSat),
}:
case <-ctx.Done():
return
}

+ 28
- 16
lndclient/lightning_client.go View File

@ -14,7 +14,6 @@ import (
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@ -29,8 +28,8 @@ type LightningClient interface {
GetInfo(ctx context.Context) (*Info, error)
GetFeeEstimate(ctx context.Context, amt btcutil.Amount, dest [33]byte) (
lnwire.MilliSatoshi, error)
EstimateFeeToP2WSH(ctx context.Context, amt btcutil.Amount,
confTarget int32) (btcutil.Amount, error)
ConfirmedWalletBalance(ctx context.Context) (btcutil.Amount, error)
@ -144,28 +143,35 @@ func (s *lightningClient) GetInfo(ctx context.Context) (*Info, error) {
}, nil
}
func (s *lightningClient) GetFeeEstimate(ctx context.Context, amt btcutil.Amount,
dest [33]byte) (lnwire.MilliSatoshi, error) {
func (s *lightningClient) EstimateFeeToP2WSH(ctx context.Context,
amt btcutil.Amount, confTarget int32) (btcutil.Amount,
error) {
rpcCtx, cancel := context.WithTimeout(ctx, rpcTimeout)
defer cancel()
routeResp, err := s.client.QueryRoutes(
// Generate dummy p2wsh address for fee estimation.
wsh := [32]byte{}
p2wshAddress, err := btcutil.NewAddressWitnessScriptHash(
wsh[:], s.params,
)
if err != nil {
return 0, err
}
resp, err := s.client.EstimateFee(
rpcCtx,
&lnrpc.QueryRoutesRequest{
Amt: int64(amt),
NumRoutes: 1,
PubKey: hex.EncodeToString(dest[:]),
&lnrpc.EstimateFeeRequest{
TargetConf: confTarget,
AddrToAmount: map[string]int64{
p2wshAddress.String(): int64(amt),
},
},
)
if err != nil {
return 0, err
}
if len(routeResp.Routes) == 0 {
return 0, ErrNoRouteToServer
}
return lnwire.MilliSatoshi(routeResp.Routes[0].TotalFeesMsat), nil
return btcutil.Amount(resp.FeeSat), nil
}
// PayInvoice pays an invoice.
@ -310,13 +316,19 @@ func (s *lightningClient) AddInvoice(ctx context.Context,
rpcIn := &lnrpc.Invoice{
Memo: in.Memo,
RHash: in.Hash[:],
Value: int64(in.Value),
Expiry: in.Expiry,
CltvExpiry: in.CltvExpiry,
Private: true,
}
if in.Preimage != nil {
rpcIn.RPreimage = in.Preimage[:]
}
if in.Hash != nil {
rpcIn.RHash = in.Hash[:]
}
resp, err := s.client.AddInvoice(rpcCtx, rpcIn)
if err != nil {
return lntypes.Hash{}, "", err

+ 585
- 0
loopin.go View File

@ -0,0 +1,585 @@
package loop
import (
"context"
"crypto/rand"
"crypto/sha256"
"fmt"
"time"
"github.com/lightninglabs/loop/swap"
"github.com/btcsuite/btcutil"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightningnetwork/lnd/lntypes"
)
var (
// MaxLoopInAcceptDelta configures the maximum acceptable number of
// remaining blocks until the on-chain htlc expires. This value is used
// to decide whether we want to continue with the swap parameters as
// proposed by the server. It is a protection to prevent the server from
// getting us to lock up our funds to an arbitrary point in the future.
MaxLoopInAcceptDelta = int32(1500)
// MinLoopInPublishDelta defines the minimum number of remaining blocks
// until on-chain htlc expiry required to proceed to publishing the htlc
// tx. This value isn't critical, as we could even safely publish the
// htlc after expiry. The reason we do implement this check is to
// prevent us from publishing an htlc that the server surely wouldn't
// follow up to.
MinLoopInPublishDelta = int32(10)
// TimeoutTxConfTarget defines the confirmation target for the loop in
// timeout tx.
TimeoutTxConfTarget = int32(2)
)
// loopInSwap contains all the in-memory state related to a pending loop in
// swap.
type loopInSwap struct {
swapKit
loopdb.LoopInContract
timeoutAddr btcutil.Address
}
// newLoopInSwap initiates a new loop in swap.
func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
currentHeight int32, request *LoopInRequest) (*loopInSwap, error) {
// Request current server loop in terms and use these to calculate the
// swap fee that we should subtract from the swap amount in the payment
// request that we send to the server.
quote, err := cfg.server.GetLoopInTerms(globalCtx)
if err != nil {
return nil, fmt.Errorf("loop in terms: %v", err)
}
swapFee := swap.CalcFee(
request.Amount, quote.SwapFeeBase, quote.SwapFeeRate,
)
if swapFee > request.MaxSwapFee {
logger.Warnf("Swap fee %v exceeding maximum of %v",
swapFee, request.MaxSwapFee)
return nil, ErrSwapFeeTooHigh
}
// Calculate the swap invoice amount. The prepay is added which
// effectively forces the server to pay us back our prepayment on a
// successful swap.
swapInvoiceAmt := request.Amount - swapFee
// Generate random preimage.
var swapPreimage lntypes.Preimage
if _, err := rand.Read(swapPreimage[:]); err != nil {
logger.Error("Cannot generate preimage")
}
swapHash := lntypes.Hash(sha256.Sum256(swapPreimage[:]))
// Derive a sender key for this swap.
keyDesc, err := cfg.lnd.WalletKit.DeriveNextKey(
globalCtx, swap.KeyFamily,
)
if err != nil {
return nil, err
}
var senderKey [33]byte
copy(senderKey[:], keyDesc.PubKey.SerializeCompressed())
// Create the swap invoice in lnd.
_, swapInvoice, err := cfg.lnd.Client.AddInvoice(
globalCtx, &invoicesrpc.AddInvoiceData{
Preimage: &swapPreimage,
Value: swapInvoiceAmt,
Memo: "swap",
Expiry: 3600 * 24 * 365,
},
)
if err != nil {
return nil, err
}
// Post the swap parameters to the swap server. The response contains
// the server success key and the expiry height of the on-chain swap
// htlc.
logger.Infof("Initiating swap request at height %v", currentHeight)
swapResp, err := cfg.server.NewLoopInSwap(globalCtx, swapHash,
request.Amount, senderKey, swapInvoice,
)
if err != nil {
return nil, fmt.Errorf("cannot initiate swap: %v", err)
}
// Validate the response parameters the prevent us continuing with a
// swap that is based on parameters outside our allowed range.
err = validateLoopInContract(cfg.lnd, currentHeight, request, swapResp)
if err != nil {
return nil, err
}
// Instantiate a struct that contains all required data to start the
// swap.
initiationTime := time.Now()
contract := loopdb.LoopInContract{
HtlcConfTarget: request.HtlcConfTarget,
LoopInChannel: request.LoopInChannel,
SwapContract: loopdb.SwapContract{
InitiationHeight: currentHeight,
InitiationTime: initiationTime,
ReceiverKey: swapResp.receiverKey,
SenderKey: senderKey,
Preimage: swapPreimage,
AmountRequested: request.Amount,
CltvExpiry: swapResp.expiry,
MaxMinerFee: request.MaxMinerFee,
MaxSwapFee: request.MaxSwapFee,
},
}
swapKit, err := newSwapKit(
swapHash, TypeIn, cfg, &contract.SwapContract,
)
if err != nil {
return nil, err
}
swapKit.lastUpdateTime = initiationTime
swap := &loopInSwap{
LoopInContract: contract,
swapKit: *swapKit,
}
// Persist the data before exiting this function, so that the caller can
// trust that this swap will be resumed on restart.
err = cfg.store.CreateLoopIn(swapHash, &swap.LoopInContract)
if err != nil {
return nil, fmt.Errorf("cannot store swap: %v", err)
}
return swap, nil
}
// resumeLoopInSwap returns a swap object representing a pending swap that has
// been restored from the database.
func resumeLoopInSwap(reqContext context.Context, cfg *swapConfig,
pend *loopdb.LoopIn) (*loopInSwap, error) {
hash := lntypes.Hash(sha256.Sum256(pend.Contract.Preimage[:]))
logger.Infof("Resuming loop in swap %v", hash)
swapKit, err := newSwapKit(
hash, TypeIn, cfg, &pend.Contract.SwapContract,
)
if err != nil {
return nil, err
}
swap := &loopInSwap{
LoopInContract: *pend.Contract,
swapKit: *swapKit,
}
lastUpdate := pend.LastUpdate()
if lastUpdate == nil {
swap.lastUpdateTime = pend.Contract.InitiationTime
} else {
swap.state = lastUpdate.State
swap.lastUpdateTime = lastUpdate.Time
}
return swap, nil
}
// validateLoopInContract validates the contract parameters against our
// request.
func validateLoopInContract(lnd *lndclient.LndServices,
height int32,
request *LoopInRequest,
response *newLoopInResponse) error {
// Verify that we are not forced to publish an htlc that locks up our
// funds for too long in case the server doesn't follow through.
if response.expiry-height > MaxLoopInAcceptDelta {
return ErrExpiryTooFar
}
return nil
}
// execute starts/resumes the swap. It is a thin wrapper around executeSwap to
// conveniently handle the error case.
func (s *loopInSwap) execute(mainCtx context.Context,
cfg *executeConfig, height int32) error {
s.executeConfig = *cfg
s.height = height
// Announce swap by sending out an initial update.
err := s.sendUpdate(mainCtx)
if err != nil {
return err
}
// Execute the swap until it either reaches a final state or a temporary
// error occurs.
err = s.executeSwap(mainCtx)
// Sanity check. If there is no error, the swap must be in a final
// state.
if err == nil && s.state.Type() == loopdb.StateTypePending {
err = fmt.Errorf("swap in non-final state %v", s.state)
}
// If an unexpected error happened, report a temporary failure
// but don't persist the error. Otherwise for example a
// connection error could lead to abandoning the swap
// permanently and losing funds.
if err != nil {
s.log.Errorf("Swap error: %v", err)
s.state = loopdb.StateFailTemporary
// If we cannot send out this update, there is nothing we can do.
_ = s.sendUpdate(mainCtx)
return err
}
s.log.Infof("Loop in swap completed: %v "+
"(final cost: server %v, onchain %v)",
s.state,
s.cost.Server,
s.cost.Onchain,
)
return nil
}
// executeSwap executes the swap.
func (s *loopInSwap) executeSwap(globalCtx context.Context) error {
var err error
// For loop in, the client takes the first step by publishing the
// on-chain htlc. Only do this is we haven't already done so in a
// previous run.
if s.state == loopdb.StateInitiated {
published, err := s.publishOnChainHtlc(globalCtx)
if err != nil {
return err
}
if !published {
return nil
}
}
// Wait for the htlc to confirm. After a restart this will pick up a
// previously published tx.
conf, err := s.waitForHtlcConf(globalCtx)
if err != nil {
return err
}
// Determine the htlc outpoint by inspecting the htlc tx.
htlcOutpoint, htlcValue, err := swap.GetScriptOutput(
conf.Tx, s.htlc.ScriptHash,
)
if err != nil {
return err
}
// TODO: Add miner fee of htlc tx to swap cost balance.
// The server is expected to see the htlc on-chain and knowing that it
// can sweep that htlc with the preimage, it should pay our swap
// invoice, receive the preimage and sweep the htlc. We are waiting for
// this to happen and simultaneously watch the htlc expiry height. When
// the htlc expires, we will publish a timeout tx to reclaim the funds.
spend, err := s.waitForHtlcSpend(globalCtx, htlcOutpoint)
if err != nil {
return err
}
// Determine the htlc input of the spending tx and inspect the witness
// to findout whether a success or a timeout tx spend the htlc.
htlcInput := spend.SpendingTx.TxIn[spend.SpenderInputIndex]
if s.htlc.IsSuccessWitness(htlcInput.Witness) {
s.state = loopdb.StateSuccess
// Server swept the htlc. The htlc value can be added to the
// server cost balance.
s.cost.Server += htlcValue
} else {
s.state = loopdb.StateFailTimeout
// Now that the timeout tx confirmed, we can safely cancel the
// swap invoice. We still need to query the final invoice state.
// This is not a hodl invoice, so it may be that the invoice was
// already settled. This means that the server didn't succeed in
// sweeping the htlc after paying the invoice.
err := s.lnd.Invoices.CancelInvoice(globalCtx, s.hash)
if err != nil && err != channeldb.ErrInvoiceAlreadySettled {
return err
}
// TODO: Add miner fee of timeout tx to swap cost balance.
}
// Wait for a final state of the swap invoice. It should either be
// settled because the server successfully paid it or canceled because
// we canceled after our timeout tx confirmed.
err = s.waitForSwapInvoiceResult(globalCtx)
if err != nil {
return err
}
// Persist swap outcome.
if err := s.persistState(globalCtx); err != nil {
return err
}
return nil
}
// waitForHtlcConf watches the chain until the htlc confirms.
func (s *loopInSwap) waitForHtlcConf(globalCtx context.Context) (
*chainntnfs.TxConfirmation, error) {
ctx, cancel := context.WithCancel(globalCtx)
defer cancel()
confChan, confErr, err := s.lnd.ChainNotifier.RegisterConfirmationsNtfn(
ctx, nil, s.htlc.ScriptHash, 1, s.InitiationHeight,
)
if err != nil {
return nil, err
}
for {
select {
// Htlc confirmed.
case conf := <-confChan:
return conf, nil
// Conf ntfn error.
case err := <-confErr:
return nil, err
// Keep up with block height.
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
// Cancel.
case <-globalCtx.Done():
return nil, globalCtx.Err()
}
}
}
// publishOnChainHtlc checks whether there are still enough blocks left and if
// so, it publishes the htlc and advances the swap state.
func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
var err error
blocksRemaining := s.CltvExpiry - s.height
s.log.Infof("Blocks left until on-chain expiry: %v", blocksRemaining)
// Verify whether it still makes sense to publish the htlc.
if blocksRemaining < MinLoopInPublishDelta {
s.state = loopdb.StateFailTimeout
return false, s.persistState(ctx)
}
// Get fee estimate from lnd.
feeRate, err := s.lnd.WalletKit.EstimateFee(
ctx, s.LoopInContract.HtlcConfTarget,
)
if err != nil {
return false, fmt.Errorf("estimate fee: %v", err)
}
// Transition to state HtlcPublished before calling SendOutputs to
// prevent us from ever paying multiple times after a crash.
s.state = loopdb.StateHtlcPublished
err = s.persistState(ctx)
if err != nil {
return false, err
}
s.log.Infof("Publishing on chain HTLC with fee rate %v", feeRate)
tx, err := s.lnd.WalletKit.SendOutputs(ctx,
[]*wire.TxOut{{
PkScript: s.htlc.ScriptHash,
Value: int64(s.LoopInContract.AmountRequested),
}},
feeRate,
)
if err != nil {
return false, fmt.Errorf("send outputs: %v", err)
}
s.log.Infof("Published on chain HTLC tx %v", tx.TxHash())
return true, nil
}
// waitForHtlcSpend waits until a spending tx of the htlc gets confirmed and
// returns the spend details.
func (s *loopInSwap) waitForHtlcSpend(ctx context.Context,
htlc *wire.OutPoint) (*chainntnfs.SpendDetail, error) {
// Register the htlc spend notification.
rpcCtx, cancel := context.WithCancel(ctx)
defer cancel()
spendChan, spendErr, err := s.lnd.ChainNotifier.RegisterSpendNtfn(
rpcCtx, nil, s.htlc.ScriptHash, s.InitiationHeight,
)
if err != nil {
return nil, fmt.Errorf("register spend ntfn: %v", err)
}
for {
select {
// Spend notification error.
case err := <-spendErr:
return nil, err
case notification := <-s.blockEpochChan:
s.height = notification.(int32)
if s.height >= s.LoopInContract.CltvExpiry {
err := s.publishTimeoutTx(ctx, htlc)
if err != nil {
return nil, err
}
}
// Htlc spend, break loop.
case spendDetails := <-spendChan:
s.log.Infof("Htlc spend by tx: %v",
spendDetails.SpenderTxHash)
return spendDetails, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
// waitForSwapPaid waits until our swap invoice gets paid by the server.
func (s *loopInSwap) waitForSwapInvoiceResult(ctx context.Context) error {
// Wait for swap invoice to be paid.
rpcCtx, cancel := context.WithCancel(ctx)
defer cancel()
s.log.Infof("Subscribing to swap invoice %v", s.hash)
swapInvoiceChan, swapInvoiceErr, err := s.lnd.Invoices.SubscribeSingleInvoice(
rpcCtx, s.hash,
)
if err != nil {
return err
}
for {
select {
// Swap invoice ntfn error.
case err := <-swapInvoiceErr:
return err
case update := <-swapInvoiceChan:
s.log.Infof("Received swap invoice update: %v",
update.State)
switch update.State {
// Swap invoice was paid, so update server cost balance.
case channeldb.ContractSettled:
s.cost.Server -= update.AmtPaid
return nil
// Canceled invoice has no effect on server cost
// balance.
case channeldb.ContractCanceled:
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
}
// publishTimeoutTx publishes a timeout tx after the on-chain htlc has expired.
// The swap failed and we are reclaiming our funds.
func (s *loopInSwap) publishTimeoutTx(ctx context.Context,
htlc *wire.OutPoint) error {
if s.timeoutAddr == nil {
var err error
s.timeoutAddr, err = s.lnd.WalletKit.NextAddr(ctx)
if err != nil {
return err
}
}
// Calculate sweep tx fee
fee, err := s.sweeper.GetSweepFee(
ctx, s.htlc.MaxTimeoutWitnessSize, TimeoutTxConfTarget,
)
if err != nil {
return err
}
witnessFunc := func(sig []byte) (wire.TxWitness, error) {
return s.htlc.GenTimeoutWitness(sig)
}
timeoutTx, err := s.sweeper.CreateSweepTx(
ctx, s.height, s.htlc, *htlc, s.SenderKey, witnessFunc,
s.LoopInContract.AmountRequested, fee, s.timeoutAddr,
)
if err != nil {
return err
}
timeoutTxHash := timeoutTx.TxHash()
s.log.Infof("Publishing timeout tx %v with fee %v to addr %v",
timeoutTxHash, fee, s.timeoutAddr)
err = s.lnd.WalletKit.PublishTransaction(ctx, timeoutTx)
if err != nil {
s.log.Warnf("publish timeout: %v", err)
}
return nil
}
// persistState updates the swap state and sends out an update notification.
func (s *loopInSwap) persistState(ctx context.Context) error {
updateTime := time.Now()
s.lastUpdateTime = updateTime
// Update state in store.
err := s.store.UpdateLoopIn(s.hash, updateTime, s.state)
if err != nil {
return err
}
// Send out swap update
return s.sendUpdate(ctx)
}

+ 370
- 0
loopin_test.go View File

<
@ -0,0 +1,370 @@
package loop
import (
"context"
"testing"
"github.com/lightninglabs/loop/lndclient"
"github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
)
var (
testLoopInRequest = LoopInRequest{
Amount: btcutil.Amount(50000),
MaxSwapFee: btcutil.Amount(1000),
HtlcConfTarget: 2,
}
)
// TestLoopInSuccess tests the success scenario where the swap completes the
// happy flow.
func TestLoopInSuccess(t *testing.T) {
defer test.Guard(t)()
ctx := newLoopInTestContext(t)
height := int32(600)
cfg := &swapConfig{
lnd: &ctx.lnd.LndServices,
store: ctx.store,
server: ctx.server,
}
swap, err := newLoopInSwap(
context.Background(), cfg,
height, &testLoopInRequest,
)
if err != nil {
t.Fatal(err)
}
ctx.store.assertLoopInStored()
errChan := make(chan error)
go func() {
err := swap.execute(context.Background(), ctx.cfg, height)
if err != nil {
logger.Error(err)
}
errChan <- err
}()
ctx.assertState(loopdb.StateInitiated)
ctx.assertState(loopdb.StateHtlcPublished)
ctx.store.assertLoopInState(loopdb.StateHtlcPublished)
// Expect htlc to be published.
htlcTx := <-ctx.lnd.SendOutputsChannel
// Expect register for htlc conf.
<-ctx.lnd.RegisterConfChannel
// Confirm htlc.
ctx.lnd.ConfChannel <- &chainntnfs.TxConfirmation{
Tx: &htlcTx,
}
// Client starts listening for spend of htlc.
<-ctx.lnd.RegisterSpendChannel
// Server spends htlc.
successTx := wire.MsgTx{}
successTx.AddTxIn(&wire.TxIn{
Witness: [][]byte{{}, {}, {}},
})
ctx.lnd.SpendChannel <- &chainntnfs.SpendDetail{
SpendingTx: &successTx,
SpenderInputIndex: 0,
}
// Client starts listening for swap invoice updates.
subscription := <-ctx.lnd.SingleInvoiceSubcribeChannel
if subscription.Hash != ctx.server.swapHash {
t.Fatal("client subscribing to wrong invoice")
}
// Server has already paid invoice before spending the htlc. Signal
// settled.
subscription.Update <- lndclient.InvoiceUpdate{
State: channeldb.ContractSettled,
AmtPaid: 49000,
}
ctx.assertState(loopdb.StateSuccess)
ctx.store.assertLoopInState(loopdb.StateSuccess)
err = <-errChan
if err != nil {
t.Fatal(err)
}
}
// TestLoopInTimeout tests the scenario where the server doesn't sweep the htlc
// and the client is forced to reclaim the funds using the timeout tx.
func TestLoopInTimeout(t *testing.T) {
defer test.Guard(t)()
ctx := newLoopInTestContext(t)
height := int32(600)