Skip to content

Commit d277bff

Browse files
authored
Merge pull request #786 from hieblmi/static-addr-7
[7/?] StaticAddr: Loop-In
2 parents f959936 + ac65726 commit d277bff

Some content is hidden

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

46 files changed

+7547
-1024
lines changed

client.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ var (
4343
ErrSwapAmountTooHigh = errors.New("swap amount too high")
4444

4545
// ErrExpiryTooFar is returned when the server proposes an expiry that
46-
// is too soon for us.
46+
// is too far in the future.
4747
ErrExpiryTooFar = errors.New("swap expiry too far")
4848

49+
// ErrExpiryTooSoon is returned when the server proposes an expiry that
50+
// is too soon.
51+
ErrExpiryTooSoon = errors.New("swap expiry too soon")
52+
4953
// ErrInsufficientBalance indicates insufficient confirmed balance to
5054
// publish a swap.
5155
ErrInsufficientBalance = errors.New("insufficient confirmed balance")
@@ -131,6 +135,22 @@ type ClientConfig struct {
131135
// MaxPaymentRetries is the maximum times we retry an off-chain payment
132136
// (used in loop out).
133137
MaxPaymentRetries int
138+
139+
// MaxStaticAddrHtlcFeePercentage is the percentage of the swap amount
140+
// that we allow the server to charge for the htlc transaction.
141+
// Although highly unlikely, this is a defense against the server
142+
// publishing the htlc without paying the swap invoice, forcing us to
143+
// sweep the timeout path.
144+
MaxStaticAddrHtlcFeePercentage float64
145+
146+
// MaxStaticAddrHtlcBackupFeePercentage is the percentage of the swap
147+
// amount that we allow the server to charge for the htlc backup
148+
// transactions. This is a defense against the server publishing the
149+
// htlc backup without paying the swap invoice, forcing us to sweep the
150+
// timeout path. This value is elevated compared to
151+
// MaxStaticAddrHtlcFeePercentage since it serves the server as backup
152+
// transaction in case of fee spikes.
153+
MaxStaticAddrHtlcBackupFeePercentage float64
134154
}
135155

136156
// NewClient returns a new instance to initiate swaps with.

cmd/loop/loopin.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ var (
5252
Name: "in",
5353
Usage: "perform an on-chain to off-chain swap (loop in)",
5454
ArgsUsage: "amt",
55-
Subcommands: []cli.Command{
56-
staticAddressCommands,
57-
},
5855
Description: `
5956
Send the amount in satoshis specified by the amt argument
6057
off-chain.

cmd/loop/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,12 @@ func displayInDetails(req *looprpc.QuoteRequest,
280280
"wallet.\n\n")
281281
}
282282

283+
if req.DepositOutpoints != nil {
284+
fmt.Printf("On-chain fees for static address loop-ins are not " +
285+
"included.\nThey were already paid when the deposits " +
286+
"were created.\n\n")
287+
}
288+
283289
printQuoteInResp(req, resp, verbose)
284290

285291
fmt.Printf("\nCONTINUE SWAP? (y/n): ")

cmd/loop/quote.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,11 @@ func printQuoteInResp(req *looprpc.QuoteRequest,
228228

229229
totalFee := resp.HtlcPublishFeeSat + resp.SwapFeeSat
230230

231-
fmt.Printf(satAmtFmt, "Send on-chain:", req.Amt)
231+
if req.DepositOutpoints != nil {
232+
fmt.Printf(satAmtFmt, "Previously deposited on-chain:", req.Amt)
233+
} else {
234+
fmt.Printf(satAmtFmt, "Send on-chain:", req.Amt)
235+
}
232236
fmt.Printf(satAmtFmt, "Receive off-chain:", req.Amt-totalFee)
233237

234238
switch {

cmd/loop/staticaddr.go

Lines changed: 231 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,55 @@ import (
99
"strings"
1010

1111
"github.com/btcsuite/btcd/chaincfg/chainhash"
12+
"github.com/lightninglabs/loop/labels"
1213
"github.com/lightninglabs/loop/looprpc"
14+
"github.com/lightninglabs/loop/staticaddr/loopin"
15+
"github.com/lightninglabs/loop/swapserverrpc"
16+
"github.com/lightningnetwork/lnd/routing/route"
1317
"github.com/urfave/cli"
1418
)
1519

1620
var staticAddressCommands = cli.Command{
1721
Name: "static",
1822
ShortName: "s",
19-
Usage: "manage static loop-in addresses",
20-
Category: "StaticAddress",
23+
Usage: "perform on-chain to off-chain swaps using static addresses.",
2124
Subcommands: []cli.Command{
2225
newStaticAddressCommand,
2326
listUnspentCommand,
2427
withdrawalCommand,
2528
summaryCommand,
2629
},
30+
Description: `
31+
Requests a loop-in swap based on static address deposits. After the
32+
creation of a static address funds can be send to it. Once the funds are
33+
confirmed on-chain they can be swapped instantaneously. If deposited
34+
funds are not needed they can we withdrawn back to the local lnd wallet.
35+
`,
36+
Flags: []cli.Flag{
37+
cli.StringSliceFlag{
38+
Name: "utxo",
39+
Usage: "specify the utxos of deposits as " +
40+
"outpoints(tx:idx) that should be looped in.",
41+
},
42+
cli.BoolFlag{
43+
Name: "all",
44+
Usage: "loop in all static address deposits.",
45+
},
46+
cli.DurationFlag{
47+
Name: "payment_timeout",
48+
Usage: "the maximum time in seconds that the server " +
49+
"is allowed to take for the swap payment. " +
50+
"The client can retry the swap with adjusted " +
51+
"parameters after the payment timed out.",
52+
},
53+
lastHopFlag,
54+
labelFlag,
55+
routeHintsFlag,
56+
privateFlag,
57+
forceFlag,
58+
verboseFlag,
59+
},
60+
Action: staticAddressLoopIn,
2761
}
2862

2963
var newStaticAddressCommand = cli.Command{
@@ -169,10 +203,11 @@ func withdraw(ctx *cli.Context) error {
169203
return fmt.Errorf("unknown withdrawal request")
170204
}
171205

172-
resp, err := client.WithdrawDeposits(ctxb, &looprpc.WithdrawDepositsRequest{
173-
Outpoints: outpoints,
174-
All: isAllSelected,
175-
})
206+
resp, err := client.WithdrawDeposits(ctxb,
207+
&looprpc.WithdrawDepositsRequest{
208+
Outpoints: outpoints,
209+
All: isAllSelected,
210+
})
176211
if err != nil {
177212
return err
178213
}
@@ -194,10 +229,14 @@ var summaryCommand = cli.Command{
194229
cli.StringFlag{
195230
Name: "filter",
196231
Usage: "specify a filter to only display deposits in " +
197-
"the specified state. The state can be one " +
198-
"of [deposited|withdrawing|withdrawn|" +
199-
"publish_expired_deposit|" +
200-
"wait_for_expiry_sweep|expired|failed].",
232+
"the specified state. Leaving out the filter " +
233+
"returns all deposits.\nThe state can be one " +
234+
"of the following: \n" +
235+
"deposited\nwithdrawing\nwithdrawn\n" +
236+
"looping_in\nlooped_in\n" +
237+
"publish_expired_deposit\n" +
238+
"sweep_htlc_timeout\nhtlc_timeout_swept\n" +
239+
"wait_for_expiry_sweep\nexpired\nfailed\n.",
201240
},
202241
},
203242
Action: summary,
@@ -229,18 +268,27 @@ func summary(ctx *cli.Context) error {
229268
case "withdrawn":
230269
filterState = looprpc.DepositState_WITHDRAWN
231270

271+
case "looping_in":
272+
filterState = looprpc.DepositState_LOOPING_IN
273+
274+
case "looped_in":
275+
filterState = looprpc.DepositState_LOOPED_IN
276+
232277
case "publish_expired_deposit":
233278
filterState = looprpc.DepositState_PUBLISH_EXPIRED
234279

280+
case "sweep_htlc_timeout":
281+
filterState = looprpc.DepositState_SWEEP_HTLC_TIMEOUT
282+
283+
case "htlc_timeout_swept":
284+
filterState = looprpc.DepositState_HTLC_TIMEOUT_SWEPT
285+
235286
case "wait_for_expiry_sweep":
236287
filterState = looprpc.DepositState_WAIT_FOR_EXPIRY_SWEEP
237288

238289
case "expired":
239290
filterState = looprpc.DepositState_EXPIRED
240291

241-
case "failed":
242-
filterState = looprpc.DepositState_FAILED_STATE
243-
244292
default:
245293
filterState = looprpc.DepositState_UNKNOWN_STATE
246294
}
@@ -297,3 +345,173 @@ func NewProtoOutPoint(op string) (*looprpc.OutPoint, error) {
297345
OutputIndex: uint32(outputIndex),
298346
}, nil
299347
}
348+
349+
func staticAddressLoopIn(ctx *cli.Context) error {
350+
if ctx.NumFlags() == 0 && ctx.NArg() == 0 {
351+
return cli.ShowAppHelp(ctx)
352+
}
353+
354+
client, cleanup, err := getClient(ctx)
355+
if err != nil {
356+
return err
357+
}
358+
defer cleanup()
359+
360+
var (
361+
ctxb = context.Background()
362+
isAllSelected = ctx.IsSet("all")
363+
isUtxoSelected = ctx.IsSet("utxo")
364+
label = ctx.String("static-loop-in")
365+
hints []*swapserverrpc.RouteHint
366+
lastHop []byte
367+
paymentTimeoutSeconds = uint32(loopin.DefaultPaymentTimeoutSeconds)
368+
)
369+
370+
// Validate our label early so that we can fail before getting a quote.
371+
if err := labels.Validate(label); err != nil {
372+
return err
373+
}
374+
375+
// Private and route hints are mutually exclusive as setting private
376+
// means we retrieve our own route hints from the connected node.
377+
hints, err = validateRouteHints(ctx)
378+
if err != nil {
379+
return err
380+
}
381+
382+
if ctx.IsSet(lastHopFlag.Name) {
383+
lastHopVertex, err := route.NewVertexFromStr(
384+
ctx.String(lastHopFlag.Name),
385+
)
386+
if err != nil {
387+
return err
388+
}
389+
390+
lastHop = lastHopVertex[:]
391+
}
392+
393+
// Get the amount we need to quote for.
394+
summaryResp, err := client.GetStaticAddressSummary(
395+
ctxb, &looprpc.StaticAddressSummaryRequest{
396+
StateFilter: looprpc.DepositState_DEPOSITED,
397+
},
398+
)
399+
if err != nil {
400+
return err
401+
}
402+
403+
var depositOutpoints []string
404+
switch {
405+
case isAllSelected == isUtxoSelected:
406+
return errors.New("must select either all or some utxos")
407+
408+
case isAllSelected:
409+
depositOutpoints = depositsToOutpoints(
410+
summaryResp.FilteredDeposits,
411+
)
412+
413+
case isUtxoSelected:
414+
depositOutpoints = ctx.StringSlice("utxo")
415+
416+
default:
417+
return fmt.Errorf("unknown quote request")
418+
}
419+
420+
if containsDuplicates(depositOutpoints) {
421+
return errors.New("duplicate outpoints detected")
422+
}
423+
424+
quoteReq := &looprpc.QuoteRequest{
425+
LoopInRouteHints: hints,
426+
LoopInLastHop: lastHop,
427+
Private: ctx.Bool(privateFlag.Name),
428+
DepositOutpoints: depositOutpoints,
429+
}
430+
quote, err := client.GetLoopInQuote(ctxb, quoteReq)
431+
if err != nil {
432+
return err
433+
}
434+
435+
limits := getInLimits(quote)
436+
437+
// populate the quote request with the sum of selected deposits and
438+
// prompt the user for acceptance.
439+
quoteReq.Amt, err = sumDeposits(
440+
depositOutpoints, summaryResp.FilteredDeposits,
441+
)
442+
if err != nil {
443+
return err
444+
}
445+
446+
if !(ctx.Bool("force") || ctx.Bool("f")) {
447+
err = displayInDetails(quoteReq, quote, ctx.Bool("verbose"))
448+
if err != nil {
449+
return err
450+
}
451+
}
452+
453+
if ctx.IsSet("payment_timeout") {
454+
paymentTimeoutSeconds = uint32(ctx.Duration("payment_timeout").Seconds())
455+
}
456+
457+
req := &looprpc.StaticAddressLoopInRequest{
458+
Outpoints: depositOutpoints,
459+
MaxSwapFeeSatoshis: int64(limits.maxSwapFee),
460+
LastHop: lastHop,
461+
Label: ctx.String(labelFlag.Name),
462+
Initiator: defaultInitiator,
463+
RouteHints: hints,
464+
Private: ctx.Bool("private"),
465+
PaymentTimeoutSeconds: paymentTimeoutSeconds,
466+
}
467+
468+
resp, err := client.StaticAddressLoopIn(ctxb, req)
469+
if err != nil {
470+
return err
471+
}
472+
473+
printRespJSON(resp)
474+
475+
return nil
476+
}
477+
478+
func containsDuplicates(outpoints []string) bool {
479+
found := make(map[string]struct{})
480+
for _, outpoint := range outpoints {
481+
if _, ok := found[outpoint]; ok {
482+
return true
483+
}
484+
found[outpoint] = struct{}{}
485+
}
486+
487+
return false
488+
}
489+
490+
func sumDeposits(outpoints []string, deposits []*looprpc.Deposit) (int64,
491+
error) {
492+
493+
var sum int64
494+
depositMap := make(map[string]*looprpc.Deposit)
495+
for _, deposit := range deposits {
496+
depositMap[deposit.Outpoint] = deposit
497+
}
498+
499+
for _, outpoint := range outpoints {
500+
if _, ok := depositMap[outpoint]; !ok {
501+
return 0, fmt.Errorf("deposit %v not found", outpoint)
502+
}
503+
504+
sum += depositMap[outpoint].Value
505+
}
506+
507+
return sum, nil
508+
}
509+
510+
func depositsToOutpoints(deposits []*looprpc.Deposit) []string {
511+
outpoints := make([]string, 0, len(deposits))
512+
for _, deposit := range deposits {
513+
outpoints = append(outpoints, deposit.Outpoint)
514+
}
515+
516+
return outpoints
517+
}

0 commit comments

Comments
 (0)