d004442efb
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.
256 lines
7.2 KiB
Go
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
|
|
}
|