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), } // The custom channel data is currently just the raw bytes of // the encoded custom records. customData, err := lnwire.CustomRecords( htlc.CustomRecords, ).Serialize() if err != nil { return nil, err } rpcHtlc.CustomChannelData = customData // 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 }