faraday/cmd/frcli/utils.go
2026-02-28 18:43:20 -05:00

376 lines
10 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/faraday"
"github.com/lightninglabs/faraday/fiat"
"github.com/lightninglabs/faraday/frdrpc"
"github.com/lightninglabs/faraday/utils"
"github.com/lightninglabs/lndclient"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/macaroons"
"github.com/urfave/cli"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/protobuf/proto"
"gopkg.in/macaroon.v2"
)
var (
// maxMsgRecvSize is the largest message our client will receive. We
// set this to 200MiB atm.
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(1 * 1024 * 1024 * 200)
// defaultMacaroonTimeout is the default macaroon timeout in seconds
// that we set when sending it over the line.
defaultMacaroonTimeout int64 = 60
)
// fatal logs and error and exits.
func fatal(err error) {
_, _ = fmt.Fprintf(os.Stderr, "[frcli] %v\n", err)
os.Exit(1)
}
// printRespJSON prints a proto message as json.
func printRespJSON(resp proto.Message) {
jsonBytes, err := lnrpc.ProtoJSONMarshalOpts.Marshal(resp)
if err != nil {
fmt.Println("unable to decode response: ", err)
return
}
fmt.Println(string(jsonBytes))
}
func printJSON(resp interface{}) {
b, err := json.Marshal(resp)
if err != nil {
fatal(err)
}
var out bytes.Buffer
_ = json.Indent(&out, b, "", "\t")
out.WriteString("\n")
_, _ = out.WriteTo(os.Stdout)
}
// getClient returns a faraday client.
func getClient(ctx *cli.Context) (frdrpc.FaradayServerClient, func()) {
conn := getClientConn(ctx)
cleanUp := func() {
if err := conn.Close(); err != nil {
fatal(err)
}
}
return frdrpc.NewFaradayServerClient(conn), cleanUp
}
// getClientConn gets a client connection to the address provided by the
// rpcserver flag.
func getClientConn(ctx *cli.Context) *grpc.ClientConn {
// Extract the paths that we need for loading the TLS and macaroon
// files.
tlsCertPath, macaroonPath, err := extractPathArgs(ctx)
if err != nil {
fatal(err)
}
// We always need to send a macaroon.
macOption, err := readMacaroon(macaroonPath)
if err != nil {
fatal(err)
}
// We need to use a custom dialer so we can also connect to unix sockets
// and not just TCP addresses.
genericDialer := clientAddressDialer(defaultRPCPort)
opts := []grpc.DialOption{
grpc.WithContextDialer(genericDialer),
grpc.WithDefaultCallOptions(maxMsgRecvSize),
macOption,
}
// TLS cannot be disabled, we'll always have a cert file to read.
creds, err := credentials.NewClientTLSFromFile(tlsCertPath, "")
if err != nil {
fatal(fmt.Errorf("unable to read tls cert (path: %v): %v",
tlsCertPath, err))
}
opts = append(opts, grpc.WithTransportCredentials(creds))
conn, err := grpc.Dial(ctx.GlobalString("rpcserver"), opts...)
if err != nil {
fatal(fmt.Errorf("unable to connect to RPC server: %v", err))
}
return conn
}
// extractPathArgs parses the TLS certificate and macaroon paths from the
// command.
func extractPathArgs(ctx *cli.Context) (string, string, error) {
// We'll start off by parsing the network. This is needed to determine
// the correct path to the TLS certificate and macaroon when not
// specified.
networkStr := strings.ToLower(ctx.GlobalString("network"))
_, err := lndclient.Network(networkStr).ChainParams()
if err != nil {
return "", "", err
}
// We'll now fetch the faradaydir so we can make a decision on how to
// properly read the macaroon and cert files. This will either be the
// default, or will have been overwritten by the end user.
faradayDir := lncfg.CleanAndExpandPath(ctx.GlobalString(
faradayDirFlag.Name,
))
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
tlsCertFlag.Name,
))
macPath := lncfg.CleanAndExpandPath(ctx.GlobalString(
macaroonPathFlag.Name,
))
// If a custom faraday directory was set, we'll also check if a custom
// path for the TLS cert and macaroon file was set as well. If not,
// we'll override the path so they can be found within the custom
// faraday directory.
if faradayDir != faraday.FaradayDirBase ||
networkStr != faraday.DefaultNetwork {
// Only overwrite the tls cert path if the user has not
// explicitly defined it.
if !ctx.GlobalIsSet(tlsCertFlag.Name) {
tlsCertPath = filepath.Join(
faradayDir, networkStr,
faraday.DefaultTLSCertFilename,
)
}
// Only overwrite the macaroon path if the user has not
// explicitly defined it.
if !ctx.GlobalIsSet(macaroonPathFlag.Name) {
macPath = filepath.Join(
faradayDir, networkStr,
faraday.DefaultMacaroonFilename,
)
}
}
return tlsCertPath, macPath, nil
}
// ClientAddressDialer parsed client address and returns a dialer.
func clientAddressDialer(defaultPort string) func(context.Context,
string) (net.Conn, error) {
return func(ctx context.Context, addr string) (net.Conn, error) {
parsedAddr, err := lncfg.ParseAddressString(
addr, defaultPort, net.ResolveTCPAddr,
)
if err != nil {
return nil, err
}
d := net.Dialer{}
return d.DialContext(
ctx, parsedAddr.Network(), parsedAddr.String(),
)
}
}
// readMacaroon tries to read the macaroon file at the specified path and create
// gRPC dial options from it.
//
// TODO(guggero): Provide this function in lnd's macaroon package and use it
// from there.
func readMacaroon(macPath string) (grpc.DialOption, error) {
// Load the specified macaroon file.
macBytes, err := ioutil.ReadFile(macPath)
if err != nil {
return nil, fmt.Errorf("unable to read macaroon path : %v", err)
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
return nil, fmt.Errorf("unable to decode macaroon: %v", err)
}
macConstraints := []macaroons.Constraint{
// We add a time-based constraint to prevent replay of the
// macaroon. It's good for 60 seconds by default to make up for
// any discrepancy between client and server clocks, but leaking
// the macaroon before it becomes invalid makes it possible for
// an attacker to reuse the macaroon. In addition, the validity
// time of the macaroon is extended by the time the server clock
// is behind the client clock, or shortened by the time the
// server clock is ahead of the client clock (or invalid
// altogether if, in the latter case, this time is more than 60
// seconds).
macaroons.TimeoutConstraint(defaultMacaroonTimeout),
}
// Apply constraints to the macaroon.
constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
if err != nil {
return nil, err
}
// Now we append the macaroon credentials to the dial options.
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
if err != nil {
return nil, fmt.Errorf("error creating macaroon credential: %v",
err)
}
return grpc.WithPerRPCCredentials(cred), nil
}
// parseChannelPoint parses a funding txid and output index from the command
// line. Both named options as well as unnamed parameters are supported.
func parseChannelPoint(ctx *cli.Context) (*wire.OutPoint, error) {
args := ctx.Args()
var txid string
switch {
case ctx.IsSet("funding_txid"):
txid = ctx.String("funding_txid")
case args.Present():
txid = args.First()
args = args.Tail()
default:
return nil, fmt.Errorf("funding txid argument missing")
}
hash, err := chainhash.NewHashFromStr(txid)
if err != nil {
return nil, err
}
channelPoint := &wire.OutPoint{
Hash: *hash,
}
switch {
case ctx.IsSet("output_index"):
channelPoint.Index = uint32(ctx.Int("output_index"))
case args.Present():
index, err := strconv.ParseUint(args.First(), 10, 32)
if err != nil {
return nil, fmt.Errorf("unable to decode output "+
"index: %v", err)
}
channelPoint.Index = uint32(index)
}
return channelPoint, nil
}
// fiatBackendBitfinex is the rpc enum value for BITFINEX.
// TODO: Replace with frdrpc.FiatBackend_BITFINEX once the frdrpc module is
// tagged and the dependency is bumped.
const fiatBackendBitfinex = frdrpc.FiatBackend(5)
// parseFiatBackend parses the user chosen fiat backend into a FiatBackend type.
func parseFiatBackend(fiatBackend string) (frdrpc.FiatBackend, error) {
switch fiatBackend {
case "":
return frdrpc.FiatBackend_UNKNOWN_FIATBACKEND, nil
case fiat.CoinCapPriceBackend.String():
return frdrpc.FiatBackend_COINCAP, nil
case fiat.CoinDeskPriceBackend.String():
return frdrpc.FiatBackend_COINDESK, nil
case fiat.CustomPriceBackend.String():
return frdrpc.FiatBackend_CUSTOM, nil
case fiat.CoinGeckoPriceBackend.String():
return frdrpc.FiatBackend_COINGECKO, nil
case fiat.BitfinexPriceBackend.String():
return fiatBackendBitfinex, nil
default:
return frdrpc.FiatBackend_UNKNOWN_FIATBACKEND, fmt.Errorf(
"unknown fiat backend",
)
}
}
// filterPrices filters a slice of prices based on given start and end
// timestamps.
func filterPrices(prices []*frdrpc.BitcoinPrice, startTime, endTime int64) (
[]*frdrpc.BitcoinPrice, error) {
// Ensure that startTime is before endTime.
if err := utils.ValidateTimeRange(
time.Unix(startTime, 0), time.Unix(endTime, 0),
utils.DisallowFutureRange,
); err != nil {
return nil, err
}
// Sort the prices by timestamp.
sort.SliceStable(prices, func(i, j int) bool {
return prices[i].PriceTimestamp < prices[j].PriceTimestamp
})
// Filter out timestamps that are not within the start time to
// end time range but ensure that the timestamp right before
// or equal to the start timestamp is kept.
//
// nolint: prealloc
var (
filteredPrices []*frdrpc.BitcoinPrice
earliestTimeStamp *frdrpc.BitcoinPrice
)
for _, p := range prices {
if p.PriceTimestamp <= uint64(startTime) {
if earliestTimeStamp == nil ||
earliestTimeStamp.PriceTimestamp <
p.PriceTimestamp {
earliestTimeStamp = p
}
continue
}
if p.PriceTimestamp >= uint64(endTime) {
continue
}
filteredPrices = append(filteredPrices, p)
}
if earliestTimeStamp == nil {
return nil, errors.New("a price point with a timestamp " +
"earlier than the given start timestamp is required")
}
return append([]*frdrpc.BitcoinPrice{earliestTimeStamp},
filteredPrices...), nil
}