loop/cmd/loop/reservations.go
Slyghtning 3c495a2a5c
cmd/loop: require explicit 'y' confirmation on reservation new
The reservation new command printed the prepay cost and asked the user
to confirm with 'y/n'. The implementation read the answer with
fmt.Scanln(&answer) and treated only the literal 'n' as a 'no'. The
return value was discarded, so:

  - On EOF / closed stdin (CI pipelines, automated wrappers, terminal
    disconnect) Scanln returned an error and answer remained the empty
    string, which is not 'n', so the command proceeded and paid the
    LN prepayment with no user confirmation.
  - The case-sensitive 'n' check also accepted 'N', 'no', 'yes', 'Y',
    or any other string as a 'yes'.

Match the convention used by the rest of the loop CLI: only continue
when the user typed exactly 'y' (or 'Y'), and treat any read error as
'no'.
2026-08-11 15:02:42 +02:00

138 lines
2.9 KiB
Go

package main
import (
"context"
"errors"
"fmt"
"strings"
"github.com/lightninglabs/loop/looprpc"
"github.com/urfave/cli/v3"
)
var (
reservationAmountFlag = &cli.Uint64Flag{
Name: "amt",
Usage: "the amount in satoshis for the reservation",
}
reservationExpiryFlag = &cli.UintFlag{
Name: "expiry",
Usage: "the relative block height at which the reservation" +
" expires",
}
)
var reservationsCommands = &cli.Command{
Name: "reservations",
Aliases: []string{"r"},
Usage: "manage reservations",
Description: `
With loopd running, you can use this command to manage your
reservations. Reservations are 2-of-2 multisig utxos that
the loop server can open to clients. The reservations are used
to enable instant swaps.
`,
Commands: []*cli.Command{
listReservationsCommand,
newReservationCommand,
},
}
var (
listReservationsCommand = &cli.Command{
Name: "list",
Aliases: []string{"l"},
Usage: "list all reservations",
ArgsUsage: "",
Description: `
List all reservations.
`,
Action: listReservations,
}
newReservationCommand = &cli.Command{
Name: "new",
Aliases: []string{"n"},
Usage: "create a new reservation",
Description: `
Create a new reservation with the given value and expiry.
`,
Action: newReservation,
Flags: []cli.Flag{
reservationAmountFlag,
reservationExpiryFlag,
},
}
)
func newReservation(ctx context.Context, cmd *cli.Command) error {
client, cleanup, err := getClient(cmd)
if err != nil {
return err
}
defer cleanup()
rpcCtx, cancel := context.WithTimeout(ctx, defaultRpcTimeout)
defer cancel()
if !cmd.IsSet(reservationAmountFlag.Name) {
return errors.New("amt flag missing")
}
if !cmd.IsSet(reservationExpiryFlag.Name) {
return errors.New("expiry flag missing")
}
quoteReq, err := client.ReservationQuote(
rpcCtx, &looprpc.ReservationQuoteRequest{
Amt: cmd.Uint64(reservationAmountFlag.Name),
Expiry: uint32(cmd.Uint(reservationExpiryFlag.Name)),
},
)
if err != nil {
return err
}
fmt.Printf(satAmtFmt, "Reservation Cost: ", quoteReq.PrepayAmt)
fmt.Printf("CONTINUE RESERVATION? (y/n): ")
var answer string
if _, err := fmt.Scanln(&answer); err != nil ||
!strings.EqualFold(answer, "y") {
return nil
}
reservationRes, err := client.ReservationRequest(
rpcCtx, &looprpc.ReservationRequestRequest{
Amt: cmd.Uint64(reservationAmountFlag.Name),
Expiry: uint32(cmd.Uint(reservationExpiryFlag.Name)),
MaxPrepayAmt: quoteReq.PrepayAmt,
},
)
if err != nil {
return err
}
printRespJSON(reservationRes)
return nil
}
func listReservations(ctx context.Context, cmd *cli.Command) error {
client, cleanup, err := getClient(cmd)
if err != nil {
return err
}
defer cleanup()
resp, err := client.ListReservations(
ctx, &looprpc.ListReservationsRequest{},
)
if err != nil {
return err
}
printRespJSON(resp)
return nil
}