Skip to content

Commit

Permalink
Record fees for utxo management transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
mrfelton committed Feb 7, 2024
1 parent 160d7f3 commit a66fa11
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 19 deletions.
79 changes: 66 additions & 13 deletions accounting/entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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
}
Expand Down
56 changes: 50 additions & 6 deletions accounting/entries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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{
Expand Down Expand Up @@ -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,
Expand All @@ -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",
Expand All @@ -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 {
Expand All @@ -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

Expand All @@ -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)
Expand Down

0 comments on commit a66fa11

Please sign in to comment.