mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-14 12:43:31 +02:00
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.
281 lines
7.1 KiB
Go
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)
|
|
}
|
|
}
|