lnd/lnrpc/invoicesrpc/utils.go
Elle Mouton 5e84ba92af
multi: add IsBlinded to lnrpc.Invoice for nicer UX
The BlindedPathConfig struct is nice for invoice creation but when we
use the Invoice message for viewing an invoice, it would be nicer to see
an "is_blinded" field.
2024-08-08 16:46:01 +02:00

350 lines
9.8 KiB
Go

package invoicesrpc
import (
"encoding/hex"
"fmt"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/chaincfg"
"github.com/lightningnetwork/lnd/invoices"
"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 *invoices.Invoice,
activeNetParams *chaincfg.Params) (*zpay32.Invoice, error) {
paymentRequest := string(invoice.PaymentRequest)
if paymentRequest == "" {
preimage := invoice.Terms.PaymentPreimage
if preimage == nil {
return &zpay32.Invoice{}, nil
}
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 *invoices.Invoice.
func CreateRPCInvoice(invoice *invoices.Invoice,
activeNetParams *chaincfg.Params) (*lnrpc.Invoice, error) {
decoded, err := decodePayReq(invoice, activeNetParams)
if err != nil {
return nil, err
}
var rHash []byte
if decoded.PaymentHash != nil {
rHash = decoded.PaymentHash[:]
}
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 == invoices.ContractSettled
var state lnrpc.Invoice_InvoiceState
switch invoice.State {
case invoices.ContractOpen:
state = lnrpc.Invoice_OPEN
case invoices.ContractSettled:
state = lnrpc.Invoice_SETTLED
case invoices.ContractCanceled:
state = lnrpc.Invoice_CANCELED
case invoices.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 invoices.HtlcStateAccepted:
state = lnrpc.InvoiceHTLCState_ACCEPTED
case invoices.HtlcStateSettled:
state = lnrpc.InvoiceHTLCState_SETTLED
case invoices.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: htlc.AMP.Record.ChildIndex(),
Hash: htlc.AMP.Hash[:],
Preimage: preimage,
}
}
// Only report resolved times if htlc is resolved.
if htlc.State != invoices.HtlcStateAccepted {
rpcHtlc.ResolveTime = htlc.ResolveTime.Unix()
}
rpcHtlcs = append(rpcHtlcs, &rpcHtlc)
}
rpcInvoice := &lnrpc.Invoice{
Memo: string(invoice.Memo),
RHash: rHash,
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: invoice.IsKeysend(),
PaymentAddr: invoice.Terms.PaymentAddr[:],
IsAmp: invoice.IsAMP(),
IsBlinded: invoice.IsBlinded(),
}
rpcInvoice.AmpInvoiceState = make(map[string]*lnrpc.AMPInvoiceState)
for setID, ampState := range invoice.AMPState {
setIDStr := hex.EncodeToString(setID[:])
var state lnrpc.InvoiceHTLCState
switch ampState.State {
case invoices.HtlcStateAccepted:
state = lnrpc.InvoiceHTLCState_ACCEPTED
case invoices.HtlcStateSettled:
state = lnrpc.InvoiceHTLCState_SETTLED
case invoices.HtlcStateCanceled:
state = lnrpc.InvoiceHTLCState_CANCELED
default:
return nil, fmt.Errorf("unknown state %v", ampState.State)
}
rpcInvoice.AmpInvoiceState[setIDStr] = &lnrpc.AMPInvoiceState{
State: state,
SettleIndex: ampState.SettleIndex,
SettleTime: ampState.SettleDate.Unix(),
AmtPaidMsat: int64(ampState.AmtPaid),
}
// If at least one of the present HTLC sets show up as being
// settled, then we'll mark the invoice itself as being
// settled.
if ampState.State == invoices.HtlcStateSettled {
rpcInvoice.Settled = true // nolint:staticcheck
rpcInvoice.State = lnrpc.Invoice_SETTLED
}
}
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
}
// CreateRPCBlindedPayments takes a set of zpay32.BlindedPaymentPath and
// converts them into a set of lnrpc.BlindedPaymentPaths.
func CreateRPCBlindedPayments(blindedPaths []*zpay32.BlindedPaymentPath) (
[]*lnrpc.BlindedPaymentPath, error) {
var res []*lnrpc.BlindedPaymentPath
for _, path := range blindedPaths {
features := path.Features.Features()
var featuresSlice []lnrpc.FeatureBit
for feature := range features {
featuresSlice = append(
featuresSlice, lnrpc.FeatureBit(feature),
)
}
if len(path.Hops) == 0 {
return nil, fmt.Errorf("each blinded path must " +
"contain at least one hop")
}
var hops []*lnrpc.BlindedHop
for _, hop := range path.Hops {
blindedNodeID := hop.BlindedNodePub.
SerializeCompressed()
hops = append(hops, &lnrpc.BlindedHop{
BlindedNode: blindedNodeID,
EncryptedData: hop.CipherText,
})
}
introNode := path.Hops[0].BlindedNodePub
firstBlindingPoint := path.FirstEphemeralBlindingPoint
blindedPath := &lnrpc.BlindedPath{
IntroductionNode: introNode.SerializeCompressed(),
BlindingPoint: firstBlindingPoint.
SerializeCompressed(),
BlindedHops: hops,
}
res = append(res, &lnrpc.BlindedPaymentPath{
BlindedPath: blindedPath,
BaseFeeMsat: uint64(path.FeeBaseMsat),
ProportionalFeeRate: path.FeeRate,
TotalCltvDelta: uint32(path.CltvExpiryDelta),
HtlcMinMsat: path.HTLCMinMsat,
HtlcMaxMsat: path.HTLCMaxMsat,
Features: featuresSlice,
})
}
return res, nil
}
// 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)
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
}