Skip to content

Commit e8005d0

Browse files
authored
Merge pull request #34 from joostjager/loopin-merge
Loop In
2 parents 79430a8 + 30c7d71 commit e8005d0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+3590
-705
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ tags that enable the swap. This enables the required lnd rpc services.
6565

6666
```
6767
cd lnd
68-
make install tags="signrpc walletrpc chainrpc"
68+
make install tags="signrpc walletrpc chainrpc invoicesrpc"
6969
```
7070

7171
Check to see if you have already installed lnd. If you have, you will need to

client.go

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ func (s *Client) FetchLoopOutSwaps() ([]*loopdb.LoopOut, error) {
118118
return s.Store.FetchLoopOutSwaps()
119119
}
120120

121+
// FetchLoopInSwaps returns a list of all swaps currently in the database.
122+
func (s *Client) FetchLoopInSwaps() ([]*loopdb.LoopIn, error) {
123+
return s.Store.FetchLoopInSwaps()
124+
}
125+
121126
// Run is a blocking call that executes all swaps. Any pending swaps are
122127
// restored from persistent storage and resumed. Subsequent updates will be
123128
// sent through the passed in statusChan. The function can be terminated by
@@ -144,7 +149,12 @@ func (s *Client) Run(ctx context.Context,
144149

145150
// Query store before starting event loop to prevent new swaps from
146151
// being treated as swaps that need to be resumed.
147-
pendingSwaps, err := s.Store.FetchLoopOutSwaps()
152+
pendingLoopOutSwaps, err := s.Store.FetchLoopOutSwaps()
153+
if err != nil {
154+
return err
155+
}
156+
157+
pendingLoopInSwaps, err := s.Store.FetchLoopInSwaps()
148158
if err != nil {
149159
return err
150160
}
@@ -154,7 +164,7 @@ func (s *Client) Run(ctx context.Context,
154164
go func() {
155165
defer s.wg.Done()
156166

157-
s.resumeSwaps(mainCtx, pendingSwaps)
167+
s.resumeSwaps(mainCtx, pendingLoopOutSwaps, pendingLoopInSwaps)
158168

159169
// Signal that new requests can be accepted. Otherwise the new
160170
// swap could already have been added to the store and read in
@@ -194,19 +204,33 @@ func (s *Client) Run(ctx context.Context,
194204

195205
// resumeSwaps restarts all pending swaps from the provided list.
196206
func (s *Client) resumeSwaps(ctx context.Context,
197-
swaps []*loopdb.LoopOut) {
207+
loopOutSwaps []*loopdb.LoopOut, loopInSwaps []*loopdb.LoopIn) {
198208

199-
for _, pend := range swaps {
209+
swapCfg := &swapConfig{
210+
lnd: s.lndServices,
211+
store: s.Store,
212+
}
213+
214+
for _, pend := range loopOutSwaps {
200215
if pend.State().Type() != loopdb.StateTypePending {
201216
continue
202217
}
203-
swapCfg := &swapConfig{
204-
lnd: s.lndServices,
205-
store: s.Store,
206-
}
207218
swap, err := resumeLoopOutSwap(ctx, swapCfg, pend)
208219
if err != nil {
209-
logger.Errorf("resuming swap: %v", err)
220+
logger.Errorf("resuming loop out swap: %v", err)
221+
continue
222+
}
223+
224+
s.executor.initiateSwap(ctx, swap)
225+
}
226+
227+
for _, pend := range loopInSwaps {
228+
if pend.State().Type() != loopdb.StateTypePending {
229+
continue
230+
}
231+
swap, err := resumeLoopInSwap(ctx, swapCfg, pend)
232+
if err != nil {
233+
logger.Errorf("resuming loop in swap: %v", err)
210234
continue
211235
}
212236

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

229253
logger.Infof("LoopOut %v to %v (channel: %v)",
230254
request.Amount, request.DestAddr,
231255
request.LoopOutChannel,
232256
)
233257

234258
if err := s.waitForInitialized(globalCtx); err != nil {
235-
return nil, err
259+
return nil, nil, err
236260
}
237261

238262
// Create a new swap object for this swap.
@@ -246,15 +270,15 @@ func (s *Client) LoopOut(globalCtx context.Context,
246270
globalCtx, swapCfg, initiationHeight, request,
247271
)
248272
if err != nil {
249-
return nil, err
273+
return nil, nil, err
250274
}
251275

252276
// Post swap to the main loop.
253277
s.executor.initiateSwap(globalCtx, swap)
254278

255279
// Return hash so that the caller can identify this swap in the updates
256280
// stream.
257-
return &swap.hash, nil
281+
return &swap.hash, swap.htlc.Address, nil
258282
}
259283

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

285309
minerFee, err := s.sweeper.GetSweepFee(
286-
ctx, swap.QuoteHtlc.MaxSuccessWitnessSize,
310+
ctx, swap.QuoteHtlc.AddSuccessToEstimator,
287311
request.SweepConfTarget,
288312
)
289313
if err != nil {
@@ -320,3 +344,85 @@ func (s *Client) waitForInitialized(ctx context.Context) error {
320344

321345
return nil
322346
}
347+
348+
// LoopIn initiates a loop in swap.
349+
func (s *Client) LoopIn(globalCtx context.Context,
350+
request *LoopInRequest) (*lntypes.Hash, btcutil.Address, error) {
351+
352+
logger.Infof("Loop in %v (channel: %v)",
353+
request.Amount,
354+
request.LoopInChannel,
355+
)
356+
357+
if err := s.waitForInitialized(globalCtx); err != nil {
358+
return nil, nil, err
359+
}
360+
361+
// Create a new swap object for this swap.
362+
initiationHeight := s.executor.height()
363+
swapCfg := swapConfig{
364+
lnd: s.lndServices,
365+
store: s.Store,
366+
server: s.Server,
367+
}
368+
swap, err := newLoopInSwap(
369+
globalCtx, &swapCfg, initiationHeight, request,
370+
)
371+
if err != nil {
372+
return nil, nil, err
373+
}
374+
375+
// Post swap to the main loop.
376+
s.executor.initiateSwap(globalCtx, swap)
377+
378+
// Return hash so that the caller can identify this swap in the updates
379+
// stream.
380+
return &swap.hash, swap.htlc.Address, nil
381+
}
382+
383+
// LoopInQuote takes an amount and returns a break down of estimated
384+
// costs for the client. Both the swap server and the on-chain fee estimator are
385+
// queried to get to build the quote response.
386+
func (s *Client) LoopInQuote(ctx context.Context,
387+
request *LoopInQuoteRequest) (*LoopInQuote, error) {
388+
389+
// Retrieve current server terms to calculate swap fee.
390+
terms, err := s.Server.GetLoopInTerms(ctx)
391+
if err != nil {
392+
return nil, err
393+
}
394+
395+
// Check amount limits.
396+
if request.Amount < terms.MinSwapAmount {
397+
return nil, ErrSwapAmountTooLow
398+
}
399+
400+
if request.Amount > terms.MaxSwapAmount {
401+
return nil, ErrSwapAmountTooHigh
402+
}
403+
404+
// Calculate swap fee.
405+
swapFee := terms.SwapFeeBase +
406+
request.Amount*btcutil.Amount(terms.SwapFeeRate)/
407+
btcutil.Amount(swap.FeeRateTotalParts)
408+
409+
// Get estimate for miner fee.
410+
minerFee, err := s.lndServices.Client.EstimateFeeToP2WSH(
411+
ctx, request.Amount, request.HtlcConfTarget,
412+
)
413+
if err != nil {
414+
return nil, err
415+
}
416+
417+
return &LoopInQuote{
418+
SwapFee: swapFee,
419+
MinerFee: minerFee,
420+
}, nil
421+
}
422+
423+
// LoopInTerms returns the terms on which the server executes swaps.
424+
func (s *Client) LoopInTerms(ctx context.Context) (
425+
*LoopInTerms, error) {
426+
427+
return s.Server.GetLoopInTerms(ctx)
428+
}

client_test.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestSuccess(t *testing.T) {
4343

4444
// Initiate uncharge.
4545

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

7171
ctx := createClientTestContext(t, nil)
7272

73-
_, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
73+
_, _, err := ctx.swapClient.LoopOut(context.Background(), testRequest)
7474
if err != nil {
7575
t.Fatal(err)
7676
}
@@ -108,7 +108,7 @@ func TestFailWrongAmount(t *testing.T) {
108108
// Modify mock for this subtest.
109109
modifier(ctx.serverMock)
110110

111-
_, err := ctx.swapClient.LoopOut(
111+
_, _, err := ctx.swapClient.LoopOut(
112112
context.Background(), testRequest,
113113
)
114114
if err != expectedErr {
@@ -188,23 +188,25 @@ func testResume(t *testing.T, expired, preimageRevealed, expectSuccess bool) {
188188
SwapInvoice: swapPayReq,
189189
SweepConfTarget: 2,
190190
MaxSwapRoutingFee: 70000,
191+
PrepayInvoice: prePayReq,
191192
SwapContract: loopdb.SwapContract{
192193
Preimage: preimage,
193194
AmountRequested: amt,
194195
CltvExpiry: 744,
195196
ReceiverKey: receiverKey,
196197
SenderKey: senderKey,
197198
MaxSwapFee: 60000,
198-
PrepayInvoice: prePayReq,
199199
MaxMinerFee: 50000,
200200
},
201201
},
202-
Events: []*loopdb.LoopOutEvent{
203-
{
204-
State: state,
202+
Loop: loopdb.Loop{
203+
Events: []*loopdb.LoopEvent{
204+
{
205+
State: state,
206+
},
205207
},
208+
Hash: hash,
206209
},
207-
Hash: hash,
208210
}
209211

210212
if expired {

cmd/loop/loopin.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/btcsuite/btcutil"
8+
"github.com/lightninglabs/loop"
9+
"github.com/lightninglabs/loop/looprpc"
10+
"github.com/urfave/cli"
11+
)
12+
13+
var loopInCommand = cli.Command{
14+
Name: "in",
15+
Usage: "perform an on-chain to off-chain swap (loop in)",
16+
ArgsUsage: "amt",
17+
Description: `
18+
Send the amount in satoshis specified by the amt argument off-chain.`,
19+
Flags: []cli.Flag{
20+
cli.Uint64Flag{
21+
Name: "amt",
22+
Usage: "the amount in satoshis to loop in",
23+
},
24+
cli.BoolFlag{
25+
Name: "external",
26+
Usage: "expect htlc to be published externally",
27+
},
28+
},
29+
Action: loopIn,
30+
}
31+
32+
func loopIn(ctx *cli.Context) error {
33+
args := ctx.Args()
34+
35+
var amtStr string
36+
switch {
37+
case ctx.IsSet("amt"):
38+
amtStr = ctx.String("amt")
39+
case ctx.NArg() > 0:
40+
amtStr = args[0]
41+
args = args.Tail()
42+
default:
43+
// Show command help if no arguments and flags were provided.
44+
cli.ShowCommandHelp(ctx, "in")
45+
return nil
46+
}
47+
48+
amt, err := parseAmt(amtStr)
49+
if err != nil {
50+
return err
51+
}
52+
53+
client, cleanup, err := getClient(ctx)
54+
if err != nil {
55+
return err
56+
}
57+
defer cleanup()
58+
59+
quote, err := client.GetLoopInQuote(
60+
context.Background(),
61+
&looprpc.QuoteRequest{
62+
Amt: int64(amt),
63+
},
64+
)
65+
if err != nil {
66+
return err
67+
}
68+
69+
limits := getInLimits(amt, quote)
70+
71+
if err := displayLimits(loop.TypeIn, amt, limits); err != nil {
72+
return err
73+
}
74+
75+
resp, err := client.LoopIn(context.Background(), &looprpc.LoopInRequest{
76+
Amt: int64(amt),
77+
MaxMinerFee: int64(limits.maxMinerFee),
78+
MaxSwapFee: int64(limits.maxSwapFee),
79+
ExternalHtlc: ctx.Bool("external"),
80+
})
81+
if err != nil {
82+
return err
83+
}
84+
85+
fmt.Printf("Swap initiated\n")
86+
fmt.Printf("ID: %v\n", resp.Id)
87+
fmt.Printf("HTLC address: %v\n", resp.HtlcAddress)
88+
fmt.Println()
89+
fmt.Printf("Run `loop monitor` to monitor progress.\n")
90+
91+
return nil
92+
}
93+
94+
func getInLimits(amt btcutil.Amount, quote *looprpc.QuoteResponse) *limits {
95+
return &limits{
96+
// Apply a multiplier to the estimated miner fee, to not get
97+
// the swap canceled because fees increased in the mean time.
98+
maxMinerFee: btcutil.Amount(quote.MinerFee) * 3,
99+
maxSwapFee: btcutil.Amount(quote.SwapFee),
100+
}
101+
}

0 commit comments

Comments
 (0)