lnd.xprv/lnrpc/invoicesrpc/utils.go
Conner Fromknecht d004442efb
lnrpc+itest: expose AMP fields on InvoiceHTLC
We also test that legacy keysend payments are promoted to AMP payments
on the receiver-sdie by asserting basic properties of the fields
returned via the rpc.
2021-03-04 10:11:21 -08:00

256 lines
7.2 KiB
Go

package invoicesrpc
import (
"encoding/hex"
"errors"
"fmt"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/zpay32"
)
// decodePayReq decodes the invoice payment request if present. This is needed,
// because not all information is stored in dedicated invoice fields. If there
// is no payment request present, a dummy request will be returned. This can
// happen with just-in-time inserted keysend invoices.
func decodePayReq(invoice *channeldb.Invoice,
activeNetParams *chaincfg.Params) (*zpay32.Invoice, error) {
paymentRequest := string(invoice.PaymentRequest)
if paymentRequest == "" {
preimage := invoice.Terms.PaymentPreimage
if preimage == nil {
return nil, errors.New("cannot reconstruct pay req")
}
hash := [32]byte(preimage.Hash())
return &zpay32.Invoice{
PaymentHash: &hash,
}, nil
}
var err error
decoded, err := zpay32.Decode(paymentRequest, activeNetParams)
if err != nil {
return nil, fmt.Errorf("unable to decode payment "+
"request: %v", err)
}
return decoded, nil
}
// CreateRPCInvoice creates an *lnrpc.Invoice from the *channeldb.Invoice.
func CreateRPCInvoice(invoice *channeldb.Invoice,
activeNetParams *chaincfg.Params) (*lnrpc.Invoice, error) {
decoded, err := decodePayReq(invoice, activeNetParams)
if err != nil {
return nil, err
}
var descHash []byte
if decoded.DescriptionHash != nil {
descHash = decoded.DescriptionHash[:]
}
fallbackAddr := ""
if decoded.FallbackAddr != nil {
fallbackAddr = decoded.FallbackAddr.String()
}
settleDate := int64(0)
if !invoice.SettleDate.IsZero() {
settleDate = invoice.SettleDate.Unix()
}
// Convert between the `lnrpc` and `routing` types.
routeHints := CreateRPCRouteHints(decoded.RouteHints)
preimage := invoice.Terms.PaymentPreimage
satAmt := invoice.Terms.Value.ToSatoshis()
satAmtPaid := invoice.AmtPaid.ToSatoshis()
isSettled := invoice.State == channeldb.ContractSettled
var state lnrpc.Invoice_InvoiceState
switch invoice.State {
case channeldb.ContractOpen:
state = lnrpc.Invoice_OPEN
case channeldb.ContractSettled:
state = lnrpc.Invoice_SETTLED
case channeldb.ContractCanceled:
state = lnrpc.Invoice_CANCELED
case channeldb.ContractAccepted:
state = lnrpc.Invoice_ACCEPTED
default:
return nil, fmt.Errorf("unknown invoice state %v",
invoice.State)
}
rpcHtlcs := make([]*lnrpc.InvoiceHTLC, 0, len(invoice.Htlcs))
for key, htlc := range invoice.Htlcs {
var state lnrpc.InvoiceHTLCState
switch htlc.State {
case channeldb.HtlcStateAccepted:
state = lnrpc.InvoiceHTLCState_ACCEPTED
case channeldb.HtlcStateSettled:
state = lnrpc.InvoiceHTLCState_SETTLED
case channeldb.HtlcStateCanceled:
state = lnrpc.InvoiceHTLCState_CANCELED
default:
return nil, fmt.Errorf("unknown state %v", htlc.State)
}
rpcHtlc := lnrpc.InvoiceHTLC{
ChanId: key.ChanID.ToUint64(),
HtlcIndex: key.HtlcID,
AcceptHeight: int32(htlc.AcceptHeight),
AcceptTime: htlc.AcceptTime.Unix(),
ExpiryHeight: int32(htlc.Expiry),
AmtMsat: uint64(htlc.Amt),
State: state,
CustomRecords: htlc.CustomRecords,
MppTotalAmtMsat: uint64(htlc.MppTotalAmt),
}
// Populate any fields relevant to AMP payments.
if htlc.AMP != nil {
rootShare := htlc.AMP.Record.RootShare()
setID := htlc.AMP.Record.SetID()
var preimage []byte
if htlc.AMP.Preimage != nil {
preimage = htlc.AMP.Preimage[:]
}
rpcHtlc.Amp = &lnrpc.AMP{
RootShare: rootShare[:],
SetId: setID[:],
ChildIndex: uint32(htlc.AMP.Record.ChildIndex()),
Hash: htlc.AMP.Hash[:],
Preimage: preimage,
}
}
// Only report resolved times if htlc is resolved.
if htlc.State != channeldb.HtlcStateAccepted {
rpcHtlc.ResolveTime = htlc.ResolveTime.Unix()
}
rpcHtlcs = append(rpcHtlcs, &rpcHtlc)
}
rpcInvoice := &lnrpc.Invoice{
Memo: string(invoice.Memo[:]),
RHash: decoded.PaymentHash[:],
Value: int64(satAmt),
ValueMsat: int64(invoice.Terms.Value),
CreationDate: invoice.CreationDate.Unix(),
SettleDate: settleDate,
Settled: isSettled,
PaymentRequest: string(invoice.PaymentRequest),
DescriptionHash: descHash,
Expiry: int64(invoice.Terms.Expiry.Seconds()),
CltvExpiry: uint64(invoice.Terms.FinalCltvDelta),
FallbackAddr: fallbackAddr,
RouteHints: routeHints,
AddIndex: invoice.AddIndex,
Private: len(routeHints) > 0,
SettleIndex: invoice.SettleIndex,
AmtPaidSat: int64(satAmtPaid),
AmtPaidMsat: int64(invoice.AmtPaid),
AmtPaid: int64(invoice.AmtPaid),
State: state,
Htlcs: rpcHtlcs,
Features: CreateRPCFeatures(invoice.Terms.Features),
IsKeysend: len(invoice.PaymentRequest) == 0,
PaymentAddr: invoice.Terms.PaymentAddr[:],
}
if preimage != nil {
rpcInvoice.RPreimage = preimage[:]
}
return rpcInvoice, nil
}
// CreateRPCFeatures maps a feature vector into a list of lnrpc.Features.
func CreateRPCFeatures(fv *lnwire.FeatureVector) map[uint32]*lnrpc.Feature {
if fv == nil {
return nil
}
features := fv.Features()
rpcFeatures := make(map[uint32]*lnrpc.Feature, len(features))
for bit := range features {
rpcFeatures[uint32(bit)] = &lnrpc.Feature{
Name: fv.Name(bit),
IsRequired: bit.IsRequired(),
IsKnown: fv.IsKnown(bit),
}
}
return rpcFeatures
}
// CreateRPCRouteHints takes in the decoded form of an invoice's route hints
// and converts them into the lnrpc type.
func CreateRPCRouteHints(routeHints [][]zpay32.HopHint) []*lnrpc.RouteHint {
var res []*lnrpc.RouteHint
for _, route := range routeHints {
hopHints := make([]*lnrpc.HopHint, 0, len(route))
for _, hop := range route {
pubKey := hex.EncodeToString(
hop.NodeID.SerializeCompressed(),
)
hint := &lnrpc.HopHint{
NodeId: pubKey,
ChanId: hop.ChannelID,
FeeBaseMsat: hop.FeeBaseMSat,
FeeProportionalMillionths: hop.FeeProportionalMillionths,
CltvExpiryDelta: uint32(hop.CLTVExpiryDelta),
}
hopHints = append(hopHints, hint)
}
routeHint := &lnrpc.RouteHint{HopHints: hopHints}
res = append(res, routeHint)
}
return res
}
// CreateZpay32HopHints takes in the lnrpc form of route hints and converts them
// into an invoice decoded form.
func CreateZpay32HopHints(routeHints []*lnrpc.RouteHint) ([][]zpay32.HopHint, error) {
var res [][]zpay32.HopHint
for _, route := range routeHints {
hopHints := make([]zpay32.HopHint, 0, len(route.HopHints))
for _, hop := range route.HopHints {
pubKeyBytes, err := hex.DecodeString(hop.NodeId)
if err != nil {
return nil, err
}
p, err := btcec.ParsePubKey(pubKeyBytes, btcec.S256())
if err != nil {
return nil, err
}
hopHints = append(hopHints, zpay32.HopHint{
NodeID: p,
ChannelID: hop.ChanId,
FeeBaseMSat: hop.FeeBaseMsat,
FeeProportionalMillionths: hop.FeeProportionalMillionths,
CLTVExpiryDelta: uint16(hop.CltvExpiryDelta),
})
}
res = append(res, hopHints)
}
return res, nil
}