mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-13 12:33:35 +02:00
376 lines
10 KiB
Go
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
|
|
}
|