Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7/?] StaticAddr: Loop-In #786

Merged
merged 14 commits into from
Nov 5, 2024
22 changes: 21 additions & 1 deletion client.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ var (
ErrSwapAmountTooHigh = errors.New("swap amount too high")

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

// ErrExpiryTooSoon is returned when the server proposes an expiry that
// is too soon.
ErrExpiryTooSoon = errors.New("swap expiry too soon")

// ErrInsufficientBalance indicates insufficient confirmed balance to
// publish a swap.
ErrInsufficientBalance = errors.New("insufficient confirmed balance")
Expand Down Expand Up @@ -131,6 +135,22 @@ type ClientConfig struct {
// MaxPaymentRetries is the maximum times we retry an off-chain payment
// (used in loop out).
MaxPaymentRetries int

// MaxStaticAddrHtlcFeePercentage is the percentage of the swap amount
// that we allow the server to charge for the htlc transaction.
// Although highly unlikely, this is a defense against the server
// publishing the htlc without paying the swap invoice, forcing us to
// sweep the timeout path.
MaxStaticAddrHtlcFeePercentage float64

// MaxStaticAddrHtlcBackupFeePercentage is the percentage of the swap
// amount that we allow the server to charge for the htlc backup
// transactions. This is a defense against the server publishing the
// htlc backup without paying the swap invoice, forcing us to sweep the
// timeout path. This value is elevated compared to
// MaxStaticAddrHtlcFeePercentage since it serves the server as backup
// transaction in case of fee spikes.
MaxStaticAddrHtlcBackupFeePercentage float64
}

// NewClient returns a new instance to initiate swaps with.
Expand Down
3 changes: 0 additions & 3 deletions cmd/loop/loopin.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ var (
Name: "in",
Usage: "perform an on-chain to off-chain swap (loop in)",
ArgsUsage: "amt",
Subcommands: []cli.Command{
staticAddressCommands,
},
Description: `
Send the amount in satoshis specified by the amt argument
off-chain.
Expand Down
6 changes: 6 additions & 0 deletions cmd/loop/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ func displayInDetails(req *looprpc.QuoteRequest,
"wallet.\n\n")
}

if req.DepositOutpoints != nil {
fmt.Printf("On-chain fees for static address loop-ins are not " +
"included.\nThey were already paid when the deposits " +
"were created.\n\n")
}

printQuoteInResp(req, resp, verbose)

fmt.Printf("\nCONTINUE SWAP? (y/n): ")
Expand Down
6 changes: 5 additions & 1 deletion cmd/loop/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,11 @@ func printQuoteInResp(req *looprpc.QuoteRequest,

totalFee := resp.HtlcPublishFeeSat + resp.SwapFeeSat

fmt.Printf(satAmtFmt, "Send on-chain:", req.Amt)
if req.DepositOutpoints != nil {
fmt.Printf(satAmtFmt, "Previously deposited on-chain:", req.Amt)
} else {
fmt.Printf(satAmtFmt, "Send on-chain:", req.Amt)
}
fmt.Printf(satAmtFmt, "Receive off-chain:", req.Amt-totalFee)

switch {
Expand Down
244 changes: 231 additions & 13 deletions cmd/loop/staticaddr.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,55 @@ import (
"strings"

"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/lightninglabs/loop/labels"
"github.com/lightninglabs/loop/looprpc"
"github.com/lightninglabs/loop/staticaddr/loopin"
"github.com/lightninglabs/loop/swapserverrpc"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli"
)

var staticAddressCommands = cli.Command{
Name: "static",
ShortName: "s",
Usage: "manage static loop-in addresses",
Category: "StaticAddress",
Usage: "perform on-chain to off-chain swaps using static addresses.",
Subcommands: []cli.Command{
newStaticAddressCommand,
listUnspentCommand,
withdrawalCommand,
summaryCommand,
},
Description: `
Requests a loop-in swap based on static address deposits. After the
creation of a static address funds can be send to it. Once the funds are
confirmed on-chain they can be swapped instantaneously. If deposited
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose to add the number of confirmations needed here.

funds are not needed they can we withdrawn back to the local lnd wallet.
`,
Flags: []cli.Flag{
cli.StringSliceFlag{
Name: "utxo",
Usage: "specify the utxos of deposits as " +
"outpoints(tx:idx) that should be looped in.",
},
cli.BoolFlag{
Name: "all",
Usage: "loop in all static address deposits.",
},
cli.DurationFlag{
Name: "payment_timeout",
Usage: "the maximum time in seconds that the server " +
"is allowed to take for the swap payment. " +
"The client can retry the swap with adjusted " +
"parameters after the payment timed out.",
},
lastHopFlag,
labelFlag,
routeHintsFlag,
privateFlag,
forceFlag,
verboseFlag,
},
Action: staticAddressLoopIn,
}

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

resp, err := client.WithdrawDeposits(ctxb, &looprpc.WithdrawDepositsRequest{
Outpoints: outpoints,
All: isAllSelected,
})
resp, err := client.WithdrawDeposits(ctxb,
&looprpc.WithdrawDepositsRequest{
Outpoints: outpoints,
All: isAllSelected,
})
if err != nil {
return err
}
Expand All @@ -194,10 +229,14 @@ var summaryCommand = cli.Command{
cli.StringFlag{
Name: "filter",
Usage: "specify a filter to only display deposits in " +
"the specified state. The state can be one " +
"of [deposited|withdrawing|withdrawn|" +
"publish_expired_deposit|" +
"wait_for_expiry_sweep|expired|failed].",
"the specified state. Leaving out the filter " +
"returns all deposits.\nThe state can be one " +
"of the following: \n" +
"deposited\nwithdrawing\nwithdrawn\n" +
"looping_in\nlooped_in\n" +
"publish_expired_deposit\n" +
"sweep_htlc_timeout\nhtlc_timeout_swept\n" +
"wait_for_expiry_sweep\nexpired\nfailed\n.",
},
},
Action: summary,
Expand Down Expand Up @@ -229,18 +268,27 @@ func summary(ctx *cli.Context) error {
case "withdrawn":
filterState = looprpc.DepositState_WITHDRAWN

case "looping_in":
filterState = looprpc.DepositState_LOOPING_IN

case "looped_in":
filterState = looprpc.DepositState_LOOPED_IN

case "publish_expired_deposit":
filterState = looprpc.DepositState_PUBLISH_EXPIRED

case "sweep_htlc_timeout":
filterState = looprpc.DepositState_SWEEP_HTLC_TIMEOUT

case "htlc_timeout_swept":
filterState = looprpc.DepositState_HTLC_TIMEOUT_SWEPT

case "wait_for_expiry_sweep":
filterState = looprpc.DepositState_WAIT_FOR_EXPIRY_SWEEP

case "expired":
filterState = looprpc.DepositState_EXPIRED

case "failed":
filterState = looprpc.DepositState_FAILED_STATE

default:
filterState = looprpc.DepositState_UNKNOWN_STATE
}
Expand Down Expand Up @@ -297,3 +345,173 @@ func NewProtoOutPoint(op string) (*looprpc.OutPoint, error) {
OutputIndex: uint32(outputIndex),
}, nil
}

func staticAddressLoopIn(ctx *cli.Context) error {
if ctx.NumFlags() == 0 && ctx.NArg() == 0 {
return cli.ShowAppHelp(ctx)
}

client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()

var (
ctxb = context.Background()
isAllSelected = ctx.IsSet("all")
isUtxoSelected = ctx.IsSet("utxo")
label = ctx.String("static-loop-in")
hints []*swapserverrpc.RouteHint
lastHop []byte
paymentTimeoutSeconds = uint32(loopin.DefaultPaymentTimeoutSeconds)
)

// Validate our label early so that we can fail before getting a quote.
if err := labels.Validate(label); err != nil {
return err
}

// Private and route hints are mutually exclusive as setting private
// means we retrieve our own route hints from the connected node.
hints, err = validateRouteHints(ctx)
if err != nil {
return err
}

if ctx.IsSet(lastHopFlag.Name) {
lastHopVertex, err := route.NewVertexFromStr(
ctx.String(lastHopFlag.Name),
)
if err != nil {
return err
}

lastHop = lastHopVertex[:]
}

// Get the amount we need to quote for.
summaryResp, err := client.GetStaticAddressSummary(
ctxb, &looprpc.StaticAddressSummaryRequest{
StateFilter: looprpc.DepositState_DEPOSITED,
},
)
if err != nil {
return err
}

var depositOutpoints []string
switch {
case isAllSelected == isUtxoSelected:
return errors.New("must select either all or some utxos")

case isAllSelected:
depositOutpoints = depositsToOutpoints(
summaryResp.FilteredDeposits,
)

case isUtxoSelected:
depositOutpoints = ctx.StringSlice("utxo")

default:
return fmt.Errorf("unknown quote request")
}

if containsDuplicates(depositOutpoints) {
return errors.New("duplicate outpoints detected")
}

quoteReq := &looprpc.QuoteRequest{
LoopInRouteHints: hints,
LoopInLastHop: lastHop,
Private: ctx.Bool(privateFlag.Name),
DepositOutpoints: depositOutpoints,
}
quote, err := client.GetLoopInQuote(ctxb, quoteReq)
if err != nil {
return err
}

limits := getInLimits(quote)

// populate the quote request with the sum of selected deposits and
// prompt the user for acceptance.
quoteReq.Amt, err = sumDeposits(
depositOutpoints, summaryResp.FilteredDeposits,
)
if err != nil {
return err
}

if !(ctx.Bool("force") || ctx.Bool("f")) {
err = displayInDetails(quoteReq, quote, ctx.Bool("verbose"))
if err != nil {
return err
}
}

if ctx.IsSet("payment_timeout") {
paymentTimeoutSeconds = uint32(ctx.Duration("payment_timeout").Seconds())
}

req := &looprpc.StaticAddressLoopInRequest{
Outpoints: depositOutpoints,
MaxSwapFeeSatoshis: int64(limits.maxSwapFee),
LastHop: lastHop,
Label: ctx.String(labelFlag.Name),
Initiator: defaultInitiator,
RouteHints: hints,
Private: ctx.Bool("private"),
PaymentTimeoutSeconds: paymentTimeoutSeconds,
}

resp, err := client.StaticAddressLoopIn(ctxb, req)
if err != nil {
return err
}

printRespJSON(resp)

return nil
}

func containsDuplicates(outpoints []string) bool {
found := make(map[string]struct{})
for _, outpoint := range outpoints {
if _, ok := found[outpoint]; ok {
return true
}
found[outpoint] = struct{}{}
}

return false
}

func sumDeposits(outpoints []string, deposits []*looprpc.Deposit) (int64,
error) {

var sum int64
depositMap := make(map[string]*looprpc.Deposit)
for _, deposit := range deposits {
depositMap[deposit.Outpoint] = deposit
}

for _, outpoint := range outpoints {
if _, ok := depositMap[outpoint]; !ok {
return 0, fmt.Errorf("deposit %v not found", outpoint)
}
starius marked this conversation as resolved.
Show resolved Hide resolved

sum += depositMap[outpoint].Value
}

return sum, nil
}

func depositsToOutpoints(deposits []*looprpc.Deposit) []string {
outpoints := make([]string, 0, len(deposits))
for _, deposit := range deposits {
outpoints = append(outpoints, deposit.Outpoint)
}

return outpoints
}
Loading
Loading