mirror of
https://github.com/lightninglabs/faraday.git
synced 2026-08-19 13:18:36 +02:00
accounting: add invoice entry with circular payment special case
Add entry generation for invoices, including the special case where we made a payment to ourselves. This is common practise for nodes that manage liquidity with circular rebalancing. We record this as a balance increase because money has moved; the amount that we gain will be offset with a circular payment entry of matching amounts when we create payment records.
This commit is contained in:
parent
b10c760ec5
commit
daefa472be
4 changed files with 167 additions and 0 deletions
|
|
@ -84,3 +84,21 @@ A fee entry represents the on chain fees we paid for a transaction.
|
|||
- TxID: The on chain transaction ID.
|
||||
- Reference: TransactionID:-1.
|
||||
- Note: Note set for fees.
|
||||
|
||||
## Off Chain Reports
|
||||
|
||||
### Receipt
|
||||
Receipts off chain represent invoices that are paid via the Lightning Network.
|
||||
|
||||
- Amount: The amount that we were paid, note that this may be greater than the original invoice value.
|
||||
- TxID: The payment hash of the invoice.
|
||||
- Reference: The preimage of the invoice.
|
||||
- Note: Optionally set if the invoice had a memo attached, was overpaid, or was a keysend.
|
||||
|
||||
### Circular Receipt
|
||||
Circular receipts record instances where we have paid one of our own invoices.
|
||||
|
||||
- Amount: The amount that we were paid, note that this may be greater than the original invoice value.
|
||||
- TxID: The payment hash of the invoice.
|
||||
- Reference: The preimage of the invoice.
|
||||
- Note: Optionally set if the invoice had a memo attached, was overpaid, or was a keysend.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
)
|
||||
|
|
@ -191,3 +193,51 @@ func onChainEntries(tx *lnrpc.Transaction,
|
|||
|
||||
return []*HarmonyEntry{txEntry, feeEntry}, nil
|
||||
}
|
||||
|
||||
// invoiceNote creates an optional note for an invoice if it had a memo, was
|
||||
// overpaid, or both.
|
||||
func invoiceNote(memo string, amt, amtPaid int64, keysend bool) string {
|
||||
var notes []string
|
||||
|
||||
if memo != "" {
|
||||
notes = append(notes, fmt.Sprintf("memo: %v", memo))
|
||||
}
|
||||
|
||||
if amt != amtPaid {
|
||||
notes = append(notes, fmt.Sprintf("invoice overpaid "+
|
||||
"original amount: %v msat, paid: %v", amt, amtPaid))
|
||||
}
|
||||
|
||||
if keysend {
|
||||
notes = append(notes, "keysend payment")
|
||||
}
|
||||
|
||||
if len(notes) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Join(notes, "/")
|
||||
}
|
||||
|
||||
// invoiceEntry creates an entry for an invoice.
|
||||
func invoiceEntry(invoice *lnrpc.Invoice, circularReceipt bool,
|
||||
convert msatToFiat) (*HarmonyEntry, error) {
|
||||
|
||||
eventType := EntryTypeReceipt
|
||||
if circularReceipt {
|
||||
eventType = EntryTypeCircularReceipt
|
||||
}
|
||||
|
||||
note := invoiceNote(
|
||||
invoice.Memo, invoice.ValueMsat, invoice.AmtPaidMsat,
|
||||
invoice.IsKeysend,
|
||||
)
|
||||
|
||||
preimage := hex.EncodeToString(invoice.RPreimage)
|
||||
hash := hex.EncodeToString(invoice.RHash)
|
||||
|
||||
return newHarmonyEntry(
|
||||
invoice.SettleDate, invoice.AmtPaidMsat, eventType,
|
||||
hash, preimage, note, false, convert,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
|
@ -88,6 +89,36 @@ var (
|
|||
TotalFees: onChainFeeSat,
|
||||
DestAddresses: []string{destAddr},
|
||||
}
|
||||
|
||||
paymentRequest = "lnbcrt10n1p0t6nmypp547evsfyrakg0nmyw59ud9cegkt99yccn5nnp4suq3ac4qyzzgevsdqqcqzpgsp54hvffpajcyddm20k3ptu53930425hpnv8m06nh5jrd6qhq53anrq9qy9qsqphhzyenspf7kfwvm3wyu04fa8cjkmvndyexlnrmh52huwa4tntppjmak703gfln76rvswmsx2cz3utsypzfx40dltesy8nj64ttgemgqtwfnj9"
|
||||
|
||||
invoiceMemo = "memo"
|
||||
|
||||
invoiceAmt = lnwire.MilliSatoshi(300)
|
||||
|
||||
invoiceOverpaidAmt = lnwire.MilliSatoshi(400)
|
||||
|
||||
invoiceSettleTime int64 = 1588159722
|
||||
|
||||
invoicePreimage = "b5f0c5ac0c873a05702d0aa63a518ecdb8f3ba786be2c4f64a5b10581da976ae"
|
||||
preimage, _ = hex.DecodeString(invoicePreimage)
|
||||
|
||||
invoiceHash = "afb2c82483ed90f9ec8ea178d2e328b2ca526313a4e61ac3808f715010424659"
|
||||
hash, _ = hex.DecodeString(invoiceHash)
|
||||
|
||||
invoice = &lnrpc.Invoice{
|
||||
Memo: invoiceMemo,
|
||||
RPreimage: preimage,
|
||||
RHash: hash,
|
||||
ValueMsat: int64(invoiceAmt),
|
||||
CreationDate: 0,
|
||||
SettleDate: invoiceSettleTime,
|
||||
PaymentRequest: paymentRequest,
|
||||
AmtPaidSat: 0,
|
||||
AmtPaidMsat: int64(invoiceOverpaidAmt),
|
||||
Htlcs: nil,
|
||||
IsKeysend: true,
|
||||
}
|
||||
)
|
||||
|
||||
// mockConvert is a mocked price function which returns mockPrice * amount.
|
||||
|
|
@ -388,3 +419,66 @@ func TestOnChainEntry(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvoiceEntry tests creation of entries for regular invoices and circular
|
||||
// receipts.
|
||||
func TestInvoiceEntry(t *testing.T) {
|
||||
getEntry := func(circular bool) *HarmonyEntry {
|
||||
note := invoiceNote(
|
||||
invoice.Memo, invoice.ValueMsat, invoice.AmtPaidMsat,
|
||||
invoice.IsKeysend,
|
||||
)
|
||||
|
||||
fiat, _ := mockConvert(int64(invoiceOverpaidAmt), 0)
|
||||
|
||||
expectedEntry := &HarmonyEntry{
|
||||
Timestamp: time.Unix(invoiceSettleTime, 0),
|
||||
Amount: invoiceOverpaidAmt,
|
||||
FiatValue: fiat,
|
||||
TxID: invoiceHash,
|
||||
Reference: invoicePreimage,
|
||||
Note: note,
|
||||
Type: EntryTypeReceipt,
|
||||
OnChain: false,
|
||||
Credit: true,
|
||||
}
|
||||
|
||||
if circular {
|
||||
expectedEntry.Type = EntryTypeCircularReceipt
|
||||
}
|
||||
|
||||
return expectedEntry
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
circular bool
|
||||
}{
|
||||
{
|
||||
name: "regular receive",
|
||||
circular: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "circular",
|
||||
circular: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
entry, err := invoiceEntry(
|
||||
invoice, test.circular, mockConvert,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expectedEntry := getEntry(test.circular)
|
||||
require.Equal(t, expectedEntry, entry)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,4 +124,9 @@ const (
|
|||
// chain routing. Note that this entry type excludes fees for channel
|
||||
// opens and closes.
|
||||
EntryTypeFee
|
||||
|
||||
// EntryTypeCircularReceipt represents an invoice that we paid to
|
||||
// ourselves. This occurs when circular payments are used to rebalance
|
||||
// channels.
|
||||
EntryTypeCircularReceipt
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue