From a66fa119d8febfc3a54950d3cd369826e1fa4254 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 | 79 +++++++++++++++++++++++++++++++------- accounting/entries_test.go | 56 ++++++++++++++++++++++++--- 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/accounting/entries.go b/accounting/entries.go index 988cad3..3de443c 100644 --- a/accounting/entries.go +++ b/accounting/entries.go @@ -231,15 +231,62 @@ 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 +} + +// createOnchainFeeEntry creates a fee entry for an on chain transaction. +func createOnchainFeeEntry(tx lndclient.Transaction, category string, + note string, 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) + + feeEntry, err := newHarmonyEntry( + tx.Timestamp, feeAmt, EntryTypeFee, + tx.TxHash, FeeReference(tx.TxHash), note, category, true, + u.getFiat, + ) + + if err != nil { + return nil, err + } + + return feeEntry, nil +} + +// utxoManagementFeeNote creates a note for utxo management fee types. +func utxoManagementFeeNote(txid string) string { + return fmt.Sprintf("fees for utxo management transaction: %v", txid) +} + // 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) + utxoManagement bool ) // Determine the type of entry we are creating. If this is a sweep, we @@ -252,6 +299,9 @@ func onChainEntries(tx lndclient.Transaction, case amtMsat > 0: entryType = EntryTypeReceipt + case isUtxoManagementTx(tx): + utxoManagement = 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 +310,17 @@ func onChainEntries(tx lndclient.Transaction, return nil, nil } + // If this is a utxo management transaction, we return a fee entry only. + if utxoManagement { + note := utxoManagementFeeNote(tx.TxHash) + feeEntry, err := createOnchainFeeEntry(tx, category, note, 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 +334,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 := createOnchainFeeEntry(tx, category, "", u) if err != nil { return nil, err } diff --git a/accounting/entries_test.go b/accounting/entries_test.go index c96e4fd..ba51acf 100644 --- a/accounting/entries_test.go +++ b/accounting/entries_test.go @@ -501,12 +501,13 @@ func TestSweepEntry(t *testing.T) { // TestOnChainEntry tests creation of entries for receipts and payments, and the // generation of a fee entry where applicable. func TestOnChainEntry(t *testing.T) { - getOnChainEntry := func(amount btcutil.Amount, - hasFee bool, label string) []*HarmonyEntry { + getOnChainEntry := func(amount btcutil.Amount, hasFee bool, + isUtxoManagement bool, label string, note string) []*HarmonyEntry { var ( - entryType EntryType - feeType = EntryTypeFee + entryType EntryType + feeType = EntryTypeFee + utxoManagement bool ) switch { @@ -516,10 +517,33 @@ func TestOnChainEntry(t *testing.T) { case amount > 0: entryType = EntryTypeReceipt + case isUtxoManagement: + utxoManagement = true + default: return nil } + if utxoManagement { + 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: note, + Type: feeType, + OnChain: true, + Credit: false, + BTCPrice: mockBTCPrice, + } + + return []*HarmonyEntry{feeEntry} + } + amt := satsToMsat(onChainAmtSat) amtMsat := lnwire.MilliSatoshi(amt) entry := &HarmonyEntry{ @@ -549,7 +573,7 @@ func TestOnChainEntry(t *testing.T) { FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, feeMsat), TxID: onChainTxID, Reference: FeeReference(onChainTxID), - Note: "", + Note: note, Type: feeType, OnChain: true, Credit: false, @@ -569,8 +593,14 @@ 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 + + // Note is the expected note on the entry. + note string }{ { name: "receive with fee", @@ -597,6 +627,13 @@ func TestOnChainEntry(t *testing.T) { amount: 0, hasFee: false, }, + { + name: "zero amount utxo management tx", + amount: 0, + hasFee: true, + isUtxoManagement: true, + note: utxoManagementFeeNote(onChainTxID), + }, } for _, test := range tests { @@ -615,6 +652,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 +668,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, test.note, ) require.Equal(t, expected, entries)