From 622e0988544868ab61e98221da64025401bb1148 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Sat, 27 Jan 2024 21:48:38 +0000 Subject: [PATCH] Record fees for utxo management transactions --- accounting/entries.go | 72 +++++++++++++++++++++++++++------ accounting/entries_test.go | 83 +++++++++++++++++++++++++++++--------- 2 files changed, 123 insertions(+), 32 deletions(-) diff --git a/accounting/entries.go b/accounting/entries.go index 988cad3..49e38d2 100644 --- a/accounting/entries.go +++ b/accounting/entries.go @@ -231,15 +231,56 @@ func sweepEntries(tx lndclient.Transaction, u entryUtils) ([]*HarmonyEntry, erro return []*HarmonyEntry{txEntry, feeEntry}, nil } +// isUtxoManagementTx checks whether a transaction is restructuring our utxos. +func isUtxoManagementTx(txn lndclient.Transaction) bool { + // Check all inputs + for _, input := range txn.PreviousOutpoints { + if !input.IsOurOutput { + return false + } + } + + // Check all outputs + for _, output := range txn.OutputDetails { + if !output.IsOurAddress { + return false + } + } + + // If all inputs and outputs belong to our wallet, it's utxo management. + return true +} + +// createFeeEntry creates a fee entry for an on chain transaction. +func createFeeEntry(tx lndclient.Transaction, u entryUtils) (*HarmonyEntry, error) { + // Total fees are expressed as a positive value in sats, we convert to + // msat here and make the value negative so that it reflects as a + // debit. + feeAmt := invertedSatsToMsats(tx.Fee) + category := getCategory(tx.Label, u.customCategories) + + feeEntry, err := newHarmonyEntry( + tx.Timestamp, feeAmt, EntryTypeFee, + tx.TxHash, FeeReference(tx.TxHash), "", category, true, + u.getFiat, + ) + + if err != nil { + return nil, err + } + + return feeEntry, nil +} + // onChainEntries produces relevant entries for an on chain transaction. func onChainEntries(tx lndclient.Transaction, u entryUtils) ([]*HarmonyEntry, error) { var ( - amtMsat = satsToMsat(tx.Amount) - entryType EntryType - feeType = EntryTypeFee - category = getCategory(tx.Label, u.customCategories) + amtMsat = satsToMsat(tx.Amount) + entryType EntryType + category = getCategory(tx.Label, u.customCategories) + selfInitiatedSweep bool ) // Determine the type of entry we are creating. If this is a sweep, we @@ -252,6 +293,9 @@ func onChainEntries(tx lndclient.Transaction, case amtMsat > 0: entryType = EntryTypeReceipt + case amtMsat == 0 && isUtxoManagementTx(tx): + selfInitiatedSweep = true + // If we have a zero amount on chain transaction, we do not create an // entry for it. This may happen when the remote party claims a htlc on // our commitment. We do not want to report 0 value transactions that @@ -260,6 +304,16 @@ func onChainEntries(tx lndclient.Transaction, return nil, nil } + // If this is a self initiated sweep, we return a fee entry only. + if selfInitiatedSweep { + feeEntry, err := createFeeEntry(tx, u) + if err != nil { + return nil, err + } + + return []*HarmonyEntry{feeEntry}, nil + } + txEntry, err := newHarmonyEntry( tx.Timestamp, amtMsat, entryType, tx.TxHash, tx.TxHash, tx.Label, category, true, u.getFiat, @@ -273,15 +327,7 @@ func onChainEntries(tx lndclient.Transaction, return []*HarmonyEntry{txEntry}, nil } - // Total fees are expressed as a positive value in sats, we convert to - // msat here and make the value negative so that it reflects as a - // debit. - feeAmt := invertedSatsToMsats(tx.Fee) - - feeEntry, err := newHarmonyEntry( - tx.Timestamp, feeAmt, feeType, tx.TxHash, - FeeReference(tx.TxHash), "", category, true, u.getFiat, - ) + feeEntry, err := createFeeEntry(tx, u) if err != nil { return nil, err } diff --git a/accounting/entries_test.go b/accounting/entries_test.go index c96e4fd..d07e989 100644 --- a/accounting/entries_test.go +++ b/accounting/entries_test.go @@ -502,11 +502,12 @@ func TestSweepEntry(t *testing.T) { // generation of a fee entry where applicable. func TestOnChainEntry(t *testing.T) { getOnChainEntry := func(amount btcutil.Amount, - hasFee bool, label string) []*HarmonyEntry { + hasFee bool, isSweep bool, label string) []*HarmonyEntry { var ( - entryType EntryType - feeType = EntryTypeFee + entryType EntryType + feeType = EntryTypeFee + selfInitiatedSweep bool ) switch { @@ -516,10 +517,33 @@ func TestOnChainEntry(t *testing.T) { case amount > 0: entryType = EntryTypeReceipt + case isSweep: + selfInitiatedSweep = true + default: return nil } + if selfInitiatedSweep { + feeAmt := satsToMsat(onChainFeeSat) + feeMsat := lnwire.MilliSatoshi(feeAmt) + + feeEntry := &HarmonyEntry{ + Timestamp: onChainTimestamp, + Amount: feeMsat, + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, feeMsat), + TxID: onChainTxID, + Reference: FeeReference(onChainTxID), + Note: "", + Type: feeType, + OnChain: true, + Credit: false, + BTCPrice: mockBTCPrice, + } + + return []*HarmonyEntry{feeEntry} + } + amt := satsToMsat(onChainAmtSat) amtMsat := lnwire.MilliSatoshi(amt) entry := &HarmonyEntry{ @@ -569,33 +593,47 @@ func TestOnChainEntry(t *testing.T) { // Whether the transaction has a fee attached. hasFee bool + // Whether the transaction is a sweep + isUtxoManagement bool + // txLabel is an optional label on the rpc transaction. txLabel string }{ { - name: "receive with fee", - amount: onChainAmtSat, - hasFee: true, + name: "receive with fee", + amount: onChainAmtSat, + hasFee: true, + isUtxoManagement: false, + }, + { + name: "receive without fee", + amount: onChainAmtSat, + hasFee: false, + isUtxoManagement: false, }, { - name: "receive without fee", - amount: onChainAmtSat, - hasFee: false, + name: "payment without fee", + amount: onChainAmtSat * -1, + hasFee: false, + isUtxoManagement: false, }, { - name: "payment without fee", - amount: onChainAmtSat * -1, - hasFee: false, + name: "payment with fee", + amount: onChainAmtSat * -1, + hasFee: true, + isUtxoManagement: false, }, { - name: "payment with fee", - amount: onChainAmtSat * -1, - hasFee: true, + name: "zero amount tx", + amount: 0, + hasFee: false, + isUtxoManagement: false, }, { - name: "zero amount tx", - amount: 0, - hasFee: false, + name: "utxo management tx", + amount: 0, + hasFee: true, + isUtxoManagement: true, }, } @@ -615,6 +653,13 @@ func TestOnChainEntry(t *testing.T) { chainTx.Fee = 0 } + chainTx.PreviousOutpoints = []*lnrpc.PreviousOutPoint{{ + IsOurOutput: test.isUtxoManagement, + }} + chainTx.OutputDetails = []*lnrpc.OutputDetail{{ + IsOurAddress: test.isUtxoManagement, + }} + // Set the label as per the test. chainTx.Label = test.txLabel @@ -624,7 +669,7 @@ func TestOnChainEntry(t *testing.T) { // Create the entries we expect based on the test // params. expected := getOnChainEntry( - test.amount, test.hasFee, test.txLabel, + test.amount, test.hasFee, test.isUtxoManagement, test.txLabel, ) require.Equal(t, expected, entries)