Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ tags that enable the swap. This enables the required lnd rpc services.

```
cd lnd
make install tags="signrpc walletrpc chainrpc"
make install tags="signrpc walletrpc chainrpc invoicesrpc"
```

Check to see if you have already installed lnd. If you have, you will need to
Expand Down
134 changes: 120 additions & 14 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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) {

for _, pend := range swaps {
swapCfg := &swapConfig{
lnd: s.lndServices,
store: s.Store,
}

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
}

Expand All @@ -224,15 +248,15 @@ func (s *Client) resumeSwaps(ctx context.Context,
//
// The return value is a hash that uniquely identifies the new swap.
func (s *Client) LoopOut(globalCtx context.Context,
request *OutRequest) (*lntypes.Hash, error) {
request *OutRequest) (*lntypes.Hash, btcutil.Address, error) {

logger.Infof("LoopOut %v to %v (channel: %v)",
request.Amount, request.DestAddr,
request.LoopOutChannel,
)

if err := s.waitForInitialized(globalCtx); err != nil {
return nil, err
return nil, nil, err
}

// Create a new swap object for this swap.
Expand All @@ -246,15 +270,15 @@ func (s *Client) LoopOut(globalCtx context.Context,
globalCtx, swapCfg, initiationHeight, request,
)
if err != nil {
return nil, err
return nil, 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
return &swap.hash, swap.htlc.Address, nil
}

// LoopOutQuote takes a LoopOut amount and returns a break down of estimated
Expand Down Expand Up @@ -283,7 +307,7 @@ func (s *Client) LoopOutQuote(ctx context.Context,
)

minerFee, err := s.sweeper.GetSweepFee(
ctx, swap.QuoteHtlc.MaxSuccessWitnessSize,
ctx, swap.QuoteHtlc.AddSuccessToEstimator,
request.SweepConfTarget,
)
if err != nil {
Expand Down Expand Up @@ -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, btcutil.Address, error) {

logger.Infof("Loop in %v (channel: %v)",
request.Amount,
request.LoopInChannel,
)

if err := s.waitForInitialized(globalCtx); err != nil {
return nil, 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, 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, swap.htlc.Address, 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)
}
18 changes: 10 additions & 8 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func TestSuccess(t *testing.T) {

// Initiate uncharge.

hash, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
hash, _, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
if err != nil {
t.Fatal(err)
}
Expand All @@ -70,7 +70,7 @@ func TestFailOffchain(t *testing.T) {

ctx := createClientTestContext(t, nil)

_, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
_, _, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -108,7 +108,7 @@ func TestFailWrongAmount(t *testing.T) {
// Modify mock for this subtest.
modifier(ctx.serverMock)

_, err := ctx.swapClient.LoopOut(
_, _, err := ctx.swapClient.LoopOut(
context.Background(), testRequest,
)
if err != expectedErr {
Expand Down Expand Up @@ -188,23 +188,25 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) {
SwapInvoice: swapPayReq,
SweepConfTarget: 2,
MaxSwapRoutingFee: 70000,
PrepayInvoice: prePayReq,
SwapContract: loopdb.SwapContract{
Preimage: preimage,
AmountRequested: amt,
CltvExpiry: 744,
ReceiverKey: receiverKey,
SenderKey: senderKey,
MaxSwapFee: 60000,
PrepayInvoice: prePayReq,
MaxMinerFee: 50000,
},
},
Events: []*loopdb.LoopOutEvent{
{
State: state,
Loop: loopdb.Loop{
Events: []*loopdb.LoopEvent{
{
State: state,
},
},
Hash: hash,
},
Hash: hash,
}

if expired {
Expand Down
101 changes: 101 additions & 0 deletions cmd/loop/loopin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package main

import (
"context"
"fmt"

"github.com/btcsuite/btcutil"
"github.com/lightninglabs/loop"
"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: "amt",
Usage: "the amount in satoshis to loop in",
},
cli.BoolFlag{
Name: "external",
Usage: "expect htlc to be published externally",
},
},
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(loop.TypeIn, amt, limits); err != nil {
return err
}

resp, err := client.LoopIn(context.Background(), &looprpc.LoopInRequest{
Amt: int64(amt),
MaxMinerFee: int64(limits.maxMinerFee),
MaxSwapFee: int64(limits.maxSwapFee),
ExternalHtlc: ctx.Bool("external"),
})
if err != nil {
return err
}

fmt.Printf("Swap initiated\n")
fmt.Printf("ID: %v\n", resp.Id)
fmt.Printf("HTLC address: %v\n", resp.HtlcAddress)
fmt.Println()
fmt.Printf("Run `loop monitor` 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),
}
}
Loading