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:
carla 2020-05-20 16:29:46 +02:00
parent b10c760ec5
commit daefa472be
No known key found for this signature in database
GPG key ID: 4CA7FE54A6213C91
4 changed files with 167 additions and 0 deletions

View file

@ -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.

View file

@ -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,
)
}

View file

@ -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)
})
}
}

View file

@ -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
)