faraday/frdrpcserver/node_audit.go
Oliver Gugger 6f4c01d0f6
frdrpc+frdrpcserver: move RPC server into own package
To avoid the frdrpc package having dependencies to other libraries and
making using the JSON/WASM stubs hard, we extract all the business logic
and RPC server code into its own package called frdrpcserver.
2022-05-02 12:24:44 +02:00

281 lines
7.1 KiB
Go

package frdrpcserver
import (
"context"
"errors"
"fmt"
"sort"
"time"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/shopspring/decimal"
"github.com/lightninglabs/faraday/accounting"
"github.com/lightninglabs/faraday/fees"
"github.com/lightninglabs/faraday/fiat"
"github.com/lightninglabs/faraday/frdrpc"
)
var (
// ErrNoCategoryName is returned if a category does not have a name.
ErrNoCategoryName = errors.New("category must have a name")
// ErrSetChain is returned when on on/off chain boolean is set for
// a category
ErrSetChain = errors.New("category must be for on chain, off chain " +
"or both")
)
// parseNodeAuditRequest parses a report request and returns the config
// required to produce a report containing on chain and off chain.
func parseNodeAuditRequest(ctx context.Context, cfg *Config,
req *frdrpc.NodeAuditRequest) (*accounting.OnChainConfig,
*accounting.OffChainConfig, error) {
start, end, err := validateTimes(req.StartTime, req.EndTime)
if err != nil {
return nil, nil, err
}
// We lookup our pubkey once so that our paid to self function does
// not need to do a lookup for every payment it checks.
info, err := cfg.Lnd.Client.GetInfo(ctx)
if err != nil {
return nil, nil, err
}
priceSourceCfg, err := priceCfgFromRPC(
req.FiatBackend, req.Granularity, false, start, end,
req.CustomPrices,
)
if err != nil {
return nil, nil, err
}
pubkey, err := route.NewVertexFromBytes(info.IdentityPubkey[:])
if err != nil {
return nil, nil, err
}
if err := validateCustomCategories(req.CustomCategories); err != nil {
return nil, nil, err
}
onChainCategories, offChainCategories, err := getCategories(
req.CustomCategories,
)
if err != nil {
return nil, nil, err
}
offChain := accounting.NewOffChainConfig(
ctx, cfg.Lnd, uint64(maxInvoiceQueries),
uint64(maxPaymentQueries), uint64(maxForwardQueries),
pubkey, start, end, req.DisableFiat, priceSourceCfg,
offChainCategories,
)
// If we have a chain connection, set our tx lookup function. Otherwise
// log a warning.
var feeLookup fees.GetDetailsFunc
if cfg.BitcoinClient != nil {
feeLookup = cfg.BitcoinClient.GetTxDetail
} else {
log.Warn("creating accounting report without bitcoin " +
"backend, some fee entries will be missing (see logs)")
}
onChain := accounting.NewOnChainConfig(
ctx, cfg.Lnd, start, end, req.DisableFiat,
feeLookup, priceSourceCfg, onChainCategories,
)
return onChain, offChain, nil
}
// validateCustomCategories validates a set of custom categories. It checks that
// each has a name, and at least one bool indicating which transactions to
// classify, as well as checking that each regex provided is unique.
func validateCustomCategories(categories []*frdrpc.CustomCategory) error {
existing := make(map[string]struct{})
for _, category := range categories {
if category.Name == "" {
return ErrNoCategoryName
}
if !category.OffChain && !category.OnChain {
return ErrSetChain
}
for _, regex := range category.LabelPatterns {
_, ok := existing[regex]
if ok {
return fmt.Errorf("duplicate category regex: "+
"%v", regex)
}
existing[regex] = struct{}{}
}
}
return nil
}
func pricePointsFromRPC(prices []*frdrpc.BitcoinPrice) ([]*fiat.Price, error) {
res := make([]*fiat.Price, len(prices))
for i, p := range prices {
price, err := decimal.NewFromString(p.Price)
if err != nil {
return nil, err
}
res[i] = &fiat.Price{
Timestamp: time.Unix(int64(p.PriceTimestamp), 0),
Price: price,
Currency: p.Currency,
}
}
return res, nil
}
// validateCustomPricePoints checks that there is at lease one price point
// in the set before the given start time.
func validateCustomPricePoints(prices []*fiat.Price,
startTime time.Time) error {
for _, price := range prices {
if price.Timestamp.Before(startTime) {
return nil
}
}
return errors.New("expected at least one price point with a " +
"timestamp preceding the given start time")
}
func getCategories(
categories []*frdrpc.CustomCategory) ([]accounting.CustomCategory,
[]accounting.CustomCategory, error) {
var onChainCategories, offChainCategories []accounting.CustomCategory
for _, category := range categories {
cust, err := accounting.NewCustomCategory(
category.Name, category.LabelPatterns,
)
if err != nil {
return nil, nil, err
}
if category.OnChain {
onChainCategories = append(onChainCategories, *cust)
}
if category.OffChain {
offChainCategories = append(offChainCategories, *cust)
}
}
return onChainCategories, offChainCategories, nil
}
func rpcReportResponse(report accounting.Report) (*frdrpc.NodeAuditResponse,
error) {
entries := make([]*frdrpc.ReportEntry, len(report))
for i, entry := range report {
rpcEntry := &frdrpc.ReportEntry{
Timestamp: uint64(entry.Timestamp.Unix()),
OnChain: entry.OnChain,
CustomCategory: entry.Category,
Amount: uint64(entry.Amount),
Credit: entry.Credit,
Asset: "BTC",
Txid: entry.TxID,
Fiat: entry.FiatValue.String(),
Reference: entry.Reference,
Note: entry.Note,
BtcPrice: &frdrpc.BitcoinPrice{
Price: entry.BTCPrice.Price.String(),
Currency: entry.BTCPrice.Currency,
},
}
if !entry.BTCPrice.Timestamp.IsZero() {
rpcEntry.BtcPrice.PriceTimestamp = uint64(
entry.BTCPrice.Timestamp.Unix(),
)
}
rpcType, err := rpcEntryType(entry.Type)
if err != nil {
return nil, err
}
rpcEntry.Type = rpcType
entries[i] = rpcEntry
}
// Sort report entries by timestamp.
sort.SliceStable(entries, func(i, j int) bool {
return entries[i].Timestamp < entries[j].Timestamp
})
return &frdrpc.NodeAuditResponse{Reports: entries}, nil
}
func rpcEntryType(t accounting.EntryType) (frdrpc.EntryType, error) {
switch t {
case accounting.EntryTypeLocalChannelOpen:
return frdrpc.EntryType_LOCAL_CHANNEL_OPEN, nil
case accounting.EntryTypeRemoteChannelOpen:
return frdrpc.EntryType_REMOTE_CHANNEL_OPEN, nil
case accounting.EntryTypeChannelOpenFee:
return frdrpc.EntryType_CHANNEL_OPEN_FEE, nil
case accounting.EntryTypeChannelClose:
return frdrpc.EntryType_CHANNEL_CLOSE, nil
case accounting.EntryTypeReceipt:
return frdrpc.EntryType_RECEIPT, nil
case accounting.EntryTypePayment:
return frdrpc.EntryType_PAYMENT, nil
case accounting.EntryTypeFee:
return frdrpc.EntryType_FEE, nil
case accounting.EntryTypeCircularReceipt:
return frdrpc.EntryType_CIRCULAR_RECEIPT, nil
case accounting.EntryTypeForward:
return frdrpc.EntryType_FORWARD, nil
case accounting.EntryTypeForwardFee:
return frdrpc.EntryType_FORWARD_FEE, nil
case accounting.EntryTypeCircularPayment:
return frdrpc.EntryType_CIRCULAR_PAYMENT, nil
case accounting.EntryTypeCircularPaymentFee:
return frdrpc.EntryType_CIRCULAR_FEE, nil
case accounting.EntryTypeSweep:
return frdrpc.EntryType_SWEEP, nil
case accounting.EntryTypeSweepFee:
return frdrpc.EntryType_SWEEP_FEE, nil
case accounting.EntryTypeChannelCloseFee:
return frdrpc.EntryType_CHANNEL_CLOSE_FEE, nil
default:
return 0, fmt.Errorf("unknown entrytype: %v", t)
}
}