mirror of https://github.com/lightninglabs/loop
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.
233 lines
5.3 KiB
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
|
|
}
|