Skip to content

Commit 46e7511

Browse files
committed
cmd: static address loop-in
1 parent e04aee4 commit 46e7511

File tree

4 files changed

+237
-10
lines changed

4 files changed

+237
-10
lines changed

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: 226 additions & 6 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{
@@ -194,10 +228,14 @@ var summaryCommand = cli.Command{
194228
cli.StringFlag{
195229
Name: "filter",
196230
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].",
231+
"the specified state. Leaving out the filter " +
232+
"returns all deposits.\nThe state can be one " +
233+
"of the following: \n" +
234+
"deposited\nwithdrawing\nwithdrawn\n" +
235+
"looping_in\nlooped_in\n" +
236+
"publish_expired_deposit\n" +
237+
"sweep_htlc_timeout\nhtlc_timeout_swept\n" +
238+
"wait_for_expiry_sweep\nexpired\nfailed\n.",
201239
},
202240
},
203241
Action: summary,
@@ -229,9 +267,21 @@ func summary(ctx *cli.Context) error {
229267
case "withdrawn":
230268
filterState = looprpc.DepositState_WITHDRAWN
231269

270+
case "looping_in":
271+
filterState = looprpc.DepositState_LOOPING_IN
272+
273+
case "looped_in":
274+
filterState = looprpc.DepositState_LOOPED_IN
275+
232276
case "publish_expired_deposit":
233277
filterState = looprpc.DepositState_PUBLISH_EXPIRED
234278

279+
case "sweep_htlc_timeout":
280+
filterState = looprpc.DepositState_SWEEP_HTLC_TIMEOUT
281+
282+
case "htlc_timeout_swept":
283+
filterState = looprpc.DepositState_HTLC_TIMEOUT_SWEPT
284+
235285
case "wait_for_expiry_sweep":
236286
filterState = looprpc.DepositState_WAIT_FOR_EXPIRY_SWEEP
237287

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

0 commit comments

Comments
 (0)