mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-13 12:33:35 +02:00
Replaced "return err" cases with wrapped errors to attach the info which may be useful when debugging.
330 lines
9.9 KiB
Go
330 lines
9.9 KiB
Go
package accounting
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightninglabs/faraday/utils"
|
|
"github.com/lightninglabs/lndclient"
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
|
"github.com/lightningnetwork/lnd/routing/route"
|
|
)
|
|
|
|
// OnChainReport produces a report of our on chain activity for a period using
|
|
// live price data. Note that this report relies on transactions returned by
|
|
// GetTransactions in lnd. If a transaction is not included in this response
|
|
// (eg, a remote party opening a channel to us), it will not be included.
|
|
func OnChainReport(ctx context.Context, cfg *OnChainConfig) (Report, error) {
|
|
// Retrieve a function which can be used to query individual prices,
|
|
// or a no-op function if we do not want prices.
|
|
getPrice, err := getConversion(
|
|
ctx, cfg.StartTime, cfg.EndTime, cfg.DisableFiat,
|
|
cfg.PriceSourceCfg,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: init conversion "+
|
|
"lookup for range [%v,%v) failed: %w", cfg.StartTime,
|
|
cfg.EndTime, err)
|
|
}
|
|
|
|
info, err := getOnChainInfo(cfg, getPrice)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: gathering on-chain "+
|
|
"data failed: %w", err)
|
|
}
|
|
|
|
return onChainReport(info)
|
|
}
|
|
|
|
// onChainInformation contains all the information we require to produce an
|
|
// on chain report.
|
|
type onChainInformation struct {
|
|
txns []lndclient.Transaction
|
|
entryUtils
|
|
sweeps map[string]bool
|
|
openedChannels map[string]channelInfo
|
|
closedChannels map[string]closedChannelInfo
|
|
}
|
|
|
|
// channelInfo contains information that is common to open and closed channels.
|
|
type channelInfo struct {
|
|
channelPoint *wire.OutPoint
|
|
capacity btcutil.Amount
|
|
pubKeyBytes route.Vertex
|
|
initiator lndclient.Initiator
|
|
channelID lnwire.ShortChannelID
|
|
}
|
|
|
|
// closedChannelInfo contains channel information which has further close info.
|
|
type closedChannelInfo struct {
|
|
channelInfo
|
|
closeType string
|
|
closeInitiator string
|
|
}
|
|
|
|
func newChannelInfo(id lnwire.ShortChannelID, chanPoint *wire.OutPoint,
|
|
pubkey route.Vertex, capacity btcutil.Amount,
|
|
initiator lndclient.Initiator) channelInfo {
|
|
|
|
return channelInfo{
|
|
channelID: id,
|
|
channelPoint: chanPoint,
|
|
pubKeyBytes: pubkey,
|
|
capacity: capacity,
|
|
initiator: initiator,
|
|
}
|
|
}
|
|
|
|
// getOnChainInfo queries lnd for all transactions relevant to our on chain
|
|
// transactions, and produces the set of information that we will need to create
|
|
// an on chain report.
|
|
func getOnChainInfo(cfg *OnChainConfig, getPrice fiatPrice) (*onChainInformation,
|
|
error) {
|
|
|
|
// Create an info struct to hold all the elements we need.
|
|
info := &onChainInformation{
|
|
entryUtils: entryUtils{
|
|
getFiat: getPrice,
|
|
getFee: cfg.GetFee,
|
|
customCategories: cfg.Categories,
|
|
},
|
|
openedChannels: make(map[string]channelInfo),
|
|
sweeps: make(map[string]bool),
|
|
closedChannels: make(map[string]closedChannelInfo),
|
|
}
|
|
|
|
onChainTxns, err := cfg.OnChainTransactions()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: listing on-chain "+
|
|
"transactions failed: %w", err)
|
|
}
|
|
|
|
// Filter our on chain transactions by start and end time. If we have
|
|
// no confirmed on chain transactions over this period, we can return
|
|
// early.
|
|
info.txns, err = filterOnChain(cfg.StartTime, cfg.EndTime, onChainTxns)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: filtering "+
|
|
"transactions for range [%v,%v) failed: %w",
|
|
cfg.StartTime, cfg.EndTime, err)
|
|
}
|
|
|
|
if len(info.txns) == 0 {
|
|
return info, nil
|
|
}
|
|
|
|
// Get our pending channels so that we do not miss channel transactions
|
|
// that may have confirmed on chain, and will thus be included in our
|
|
// set of transactions, but are still considered pending by lnd (this
|
|
// is the case for channel opens that require more than one conf, or for
|
|
// closing channels that are awaiting resolution).
|
|
pending, err := cfg.PendingChannels()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: listing pending "+
|
|
"channels failed: %w", err)
|
|
}
|
|
|
|
// We add our pending force close channels to opened and closed channels
|
|
// because it is possible that our channel was opened and closed in the
|
|
// relevant period.
|
|
for _, c := range pending.PendingForceClose {
|
|
inf := newChannelInfo(
|
|
lnwire.NewShortChanIDFromInt(0), c.ChannelPoint,
|
|
c.PubKeyBytes, c.Capacity, c.ChannelInitiator,
|
|
)
|
|
|
|
info.openedChannels[c.ChannelPoint.Hash.String()] = inf
|
|
info.closedChannels[c.CloseTxid.String()] = closedChannelInfo{
|
|
channelInfo: inf,
|
|
closeType: "force close",
|
|
closeInitiator: "unknown for pending channels",
|
|
}
|
|
}
|
|
|
|
// Add our channel open and all possible channel closes to our info set.
|
|
// We add all potential close txids in case one of them has confirmed.
|
|
for _, c := range pending.WaitingClose {
|
|
inf := newChannelInfo(
|
|
lnwire.NewShortChanIDFromInt(0), c.ChannelPoint,
|
|
c.PubKeyBytes, c.Capacity, c.ChannelInitiator,
|
|
)
|
|
|
|
info.openedChannels[c.ChannelPoint.Hash.String()] = inf
|
|
|
|
closed := closedChannelInfo{
|
|
channelInfo: inf,
|
|
closeType: lndclient.CloseTypeCooperative.String(),
|
|
closeInitiator: lndclient.InitiatorUnrecorded.String(),
|
|
}
|
|
info.closedChannels[c.LocalTxid.String()] = closed
|
|
info.closedChannels[c.RemoteTxid.String()] = closed
|
|
info.closedChannels[c.RemotePending.String()] = closed
|
|
}
|
|
|
|
// Add our pending open channel to our set of open channels so that
|
|
// we can identify pending channels in our report.
|
|
for _, c := range pending.PendingOpen {
|
|
inf := newChannelInfo(
|
|
lnwire.NewShortChanIDFromInt(0), c.ChannelPoint,
|
|
c.PubKeyBytes, c.Capacity, c.ChannelInitiator,
|
|
)
|
|
|
|
info.openedChannels[c.ChannelPoint.Hash.String()] = inf
|
|
}
|
|
|
|
// Get our opened channels and create a map of closing txid to the
|
|
// channel entry. This will be used to separate channel opens out from
|
|
// other on chain transactions.
|
|
openRPCChannels, err := cfg.OpenChannels()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: listing open "+
|
|
"channels failed: %w", err)
|
|
}
|
|
|
|
for _, channel := range openRPCChannels {
|
|
outpoint, err := utils.GetOutPointFromString(
|
|
channel.ChannelPoint,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: parsing open "+
|
|
"channel point %v failed: %w",
|
|
channel.ChannelPoint, err)
|
|
}
|
|
|
|
init := lndclient.InitiatorLocal
|
|
if !channel.Initiator {
|
|
init = lndclient.InitiatorRemote
|
|
}
|
|
|
|
inf := newChannelInfo(
|
|
lnwire.NewShortChanIDFromInt(channel.ChannelID),
|
|
outpoint, channel.PubKeyBytes, channel.Capacity, init,
|
|
)
|
|
|
|
// Add the channel to our map, keyed by txid.
|
|
info.openedChannels[outpoint.Hash.String()] = inf
|
|
}
|
|
|
|
// Get our closed channels and create a map of closing txid to closed
|
|
// channel. This will be used to separate out channel closes from other
|
|
// on chain transactions.
|
|
closedRPCChannels, err := cfg.ClosedChannels()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: listing closed "+
|
|
"channels failed: %w", err)
|
|
}
|
|
|
|
// Add our already closed channels open and closed transactions to our
|
|
// on chain info so that we will be able to detect channels that were
|
|
// opened and closed within our period.
|
|
for _, closed := range closedRPCChannels {
|
|
outpoint, err := utils.GetOutPointFromString(
|
|
closed.ChannelPoint,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: parsing "+
|
|
"closed channel point %v failed: %w",
|
|
closed.ChannelPoint, err)
|
|
}
|
|
|
|
inf := newChannelInfo(
|
|
lnwire.NewShortChanIDFromInt(closed.ChannelID),
|
|
outpoint, closed.PubKeyBytes, closed.Capacity,
|
|
closed.OpenInitiator,
|
|
)
|
|
|
|
info.openedChannels[outpoint.Hash.String()] = inf
|
|
|
|
info.closedChannels[closed.ClosingTxHash] = closedChannelInfo{
|
|
channelInfo: inf,
|
|
closeType: closed.CloseType.String(),
|
|
closeInitiator: closed.CloseInitiator.String(),
|
|
}
|
|
}
|
|
|
|
// Finally, get our list of known sweeps from lnd so that we can
|
|
// identify them separately to other on chain transactions.
|
|
sweeps, err := cfg.ListSweeps()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("on-chain report: listing sweep "+
|
|
"transactions failed: %w", err)
|
|
}
|
|
|
|
for _, sweep := range sweeps {
|
|
info.sweeps[sweep] = true
|
|
}
|
|
|
|
return info, nil
|
|
}
|
|
|
|
// onChainReport produces an on chain transaction report.
|
|
func onChainReport(info *onChainInformation) (
|
|
Report, error) {
|
|
|
|
var report Report
|
|
|
|
for _, txn := range info.txns {
|
|
// If the transaction is a channel open. The channel may be one
|
|
// of our currently open channels, or a channel open for a
|
|
// channel that has already been closed.
|
|
openChannel, ok := info.openedChannels[txn.TxHash]
|
|
if ok {
|
|
entries, err := channelOpenEntries(
|
|
openChannel, txn, info.entryUtils,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tx %v: creating "+
|
|
"channel open entries failed: %w",
|
|
txn.TxHash, err)
|
|
}
|
|
|
|
report = append(report, entries...)
|
|
continue
|
|
}
|
|
|
|
// Check whether the transaction is a channel close.
|
|
channelClose, ok := info.closedChannels[txn.TxHash]
|
|
if ok {
|
|
entries, err := closedChannelEntries(
|
|
channelClose, txn, info.entryUtils,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tx %v: creating "+
|
|
"channel close entries failed: %w",
|
|
txn.TxHash, err)
|
|
}
|
|
|
|
report = append(report, entries...)
|
|
continue
|
|
}
|
|
|
|
// Next, we check whether our transaction is a sweep, and create
|
|
// sweep entries that include looking up fees so that we do not
|
|
// miss fees that are contributed by the swept input.
|
|
if info.sweeps[txn.TxHash] {
|
|
entries, err := sweepEntries(
|
|
txn, info.entryUtils,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tx %v: creating sweep "+
|
|
"entries failed: %w", txn.TxHash, err)
|
|
}
|
|
|
|
report = append(report, entries...)
|
|
continue
|
|
}
|
|
|
|
// Finally, if the transaction is unrelated to channel opens or
|
|
// closes, we create a generic on chain entry for it.
|
|
entries, err := onChainEntries(txn, info.entryUtils)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("tx %v: creating generic "+
|
|
"on-chain entries failed: %w", txn.TxHash, err)
|
|
}
|
|
report = append(report, entries...)
|
|
}
|
|
|
|
return report, nil
|
|
}
|