You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
loop/cmd/loop/instantout.go

233 lines
5.3 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"github.com/lightninglabs/loop/instantout/reservation"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli"
)
var instantOutCommand = cli.Command{
Name: "instantout",
Usage: "perform an instant off-chain to on-chain swap (looping out)",
Description: `
Attempts to instantly loop out into the backing lnd's wallet. The amount
will be chosen via the cli.
`,
Flags: []cli.Flag{
cli.StringFlag{
Name: "channel",
Usage: "the comma-separated list of short " +
"channel IDs of the channels to loop out",
},
cli.StringFlag{
Name: "addr",
Usage: "the optional address that the looped out funds " +
"should be sent to, if let blank the funds " +
"will go to lnd's wallet",
},
},
Action: instantOut,
}
func instantOut(ctx *cli.Context) error {
// Parse outgoing channel set. Don't string split if the flag is empty.
// Otherwise, strings.Split returns a slice of length one with an empty
// element.
var outgoingChanSet []uint64
if ctx.IsSet("channel") {
chanStrings := strings.Split(ctx.String("channel"), ",")
for _, chanString := range chanStrings {
chanID, err := strconv.ParseUint(chanString, 10, 64)
if err != nil {
return fmt.Errorf("error parsing channel id "+
"\"%v\"", chanString)
}
outgoingChanSet = append(outgoingChanSet, chanID)
}
}
// First set up the swap client itself.
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
// Now we fetch all the confirmed reservations.
reservations, err := client.ListReservations(
context.Background(), &looprpc.ListReservationsRequest{},
)
if err != nil {
return err
}
var (
confirmedReservations []*looprpc.ClientReservation
totalAmt int64
idx int
)
for _, res := range reservations.Reservations {
if res.State != string(reservation.Confirmed) {
continue
}
confirmedReservations = append(confirmedReservations, res)
}
if len(confirmedReservations) == 0 {
fmt.Printf("No confirmed reservations found \n")
return nil
}
fmt.Printf("Available reservations: \n\n")
for _, res := range confirmedReservations {
idx++
fmt.Printf("Reservation %v: shortid %x, amt %v, expiry "+
"height %v \n", idx, res.ReservationId[:3], res.Amount,
res.Expiry)
totalAmt += int64(res.Amount)
}
fmt.Println()
fmt.Printf("Max amount to instant out: %v\n", totalAmt)
fmt.Println()
fmt.Println("Select reservations for instantout (e.g. '1,2,3')")
fmt.Println("Type 'ALL' to use all available reservations.")
var answer string
fmt.Scanln(&answer)
// Parse
var (
selectedReservations [][]byte
selectedAmt uint64
)
switch answer {
case "ALL":
for _, res := range confirmedReservations {
selectedReservations = append(
selectedReservations,
res.ReservationId,
)
selectedAmt += res.Amount
}
case "":
return fmt.Errorf("no reservations selected")
default:
selectedIndexes := strings.Split(answer, ",")
selectedIndexMap := make(map[int]struct{})
for _, idxStr := range selectedIndexes {
idx, err := strconv.Atoi(idxStr)
if err != nil {
return err
}
if idx < 0 {
return fmt.Errorf("invalid index %v", idx)
}
if idx > len(confirmedReservations) {
return fmt.Errorf("invalid index %v", idx)
}
if _, ok := selectedIndexMap[idx]; ok {
return fmt.Errorf("duplicate index %v", idx)
}
selectedReservations = append(
selectedReservations,
confirmedReservations[idx-1].ReservationId,
)
selectedIndexMap[idx] = struct{}{}
selectedAmt += confirmedReservations[idx-1].Amount
}
}
// Now that we have the selected reservations we can estimate the
// fee-rates.
quote, err := client.InstantOutQuote(
context.Background(), &looprpc.InstantOutQuoteRequest{
Amt: selectedAmt,
NumReservations: int32(len(selectedReservations)),
},
)
if err != nil {
return err
}
fmt.Println()
fmt.Printf(satAmtFmt, "Estimated on-chain fee:", quote.SweepFeeSat)
fmt.Printf(satAmtFmt, "Service fee:", quote.ServiceFeeSat)
fmt.Println()
fmt.Printf("CONTINUE SWAP? (y/n): ")
fmt.Scanln(&answer)
if answer != "y" {
return errors.New("swap canceled")
}
fmt.Println("Starting instant swap out")
// Now we can request the instant out swap.
instantOutRes, err := client.InstantOut(
context.Background(),
&looprpc.InstantOutRequest{
ReservationIds: selectedReservations,
OutgoingChanSet: outgoingChanSet,
DestAddr: ctx.String("addr"),
},
)
if err != nil {
return err
}
fmt.Printf("Instant out swap initiated with ID: %x, State: %v \n",
instantOutRes.InstantOutHash, instantOutRes.State)
if instantOutRes.SweepTxId != "" {
fmt.Printf("Sweepless sweep tx id: %v \n",
instantOutRes.SweepTxId)
}
return nil
}
var listInstantOutsCommand = cli.Command{
Name: "listinstantouts",
Usage: "list all instant out swaps",
Description: `
List all instant out swaps.
`,
Action: listInstantOuts,
}
func listInstantOuts(ctx *cli.Context) error {
// First set up the swap client itself.
client, cleanup, err := getClient(ctx)
if err != nil {
return err
}
defer cleanup()
resp, err := client.ListInstantOuts(
context.Background(), &looprpc.ListInstantOutsRequest{},
)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}