diff --git a/zpay32/invoice.go b/zpay32/invoice.go index fb79e3cc..f779bb48 100644 --- a/zpay32/invoice.go +++ b/zpay32/invoice.go @@ -67,6 +67,16 @@ const ( // fieldTypeC contains an optional requested final CLTV delta. fieldTypeC = 24 + + // fieldType9 contains one or more bytes for signaling features + // supported or required by the receiver. + fieldType9 = 5 +) + +var ( + // InvoiceFeatures holds the set of all known feature bits that are + // exposed as BOLT 11 features. + InvoiceFeatures = map[lnwire.FeatureBit]string{} ) // MessageSigner is passed to the Encode method to provide a signature @@ -146,6 +156,10 @@ type Invoice struct { // // NOTE: This is optional. RouteHints [][]HopHint + + // Features represents an optional field used to signal optional or + // required support for features by the receiver. + Features *lnwire.FeatureVector } // Amount is a functional option that allows callers of NewInvoice to set the @@ -663,6 +677,14 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er } invoice.RouteHints = append(invoice.RouteHints, routeHint) + case fieldType9: + if invoice.Features != nil { + // We skip the field if we have already seen a + // supported one. + continue + } + + invoice.Features, err = parseFeatures(base32Data) default: // Ignore unknown type. } @@ -874,6 +896,25 @@ func parseRouteHint(data []byte) ([]HopHint, error) { return routeHint, nil } +// parseFeatures decodes any feature bits directly from the base32 +// representation. +func parseFeatures(data []byte) (*lnwire.FeatureVector, error) { + rawFeatures := lnwire.NewRawFeatureVector() + err := rawFeatures.DecodeBase32(bytes.NewReader(data), len(data)) + if err != nil { + return nil, err + } + + fv := lnwire.NewFeatureVector(rawFeatures, InvoiceFeatures) + unknownFeatures := fv.UnknownRequiredFeatures() + if len(unknownFeatures) > 0 { + return nil, fmt.Errorf("invoice contains unknown required "+ + "features: %v", unknownFeatures) + } + + return fv, nil +} + // writeTaggedFields writes the non-nil tagged fields of the Invoice to the // base32 buffer. func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { @@ -1024,6 +1065,18 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error { return err } } + if invoice.Features != nil && invoice.Features.SerializeSize32() > 0 { + var b bytes.Buffer + err := invoice.Features.RawFeatureVector.EncodeBase32(&b) + if err != nil { + return err + } + + err = writeTaggedField(bufferBase32, fieldType9, b.Bytes()) + if err != nil { + return err + } + } return nil } diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index ce69e3a4..9b75fab7 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -24,12 +24,14 @@ import ( var ( testMillisat24BTC = lnwire.MilliSatoshi(2400000000000) testMillisat2500uBTC = lnwire.MilliSatoshi(250000000) + testMillisat25mBTC = lnwire.MilliSatoshi(2500000000) testMillisat20mBTC = lnwire.MilliSatoshi(2000000000) testPaymentHashSlice, _ = hex.DecodeString("0001020304050607080900010203040506070809000102030405060708090102") testEmptyString = "" testCupOfCoffee = "1 cup coffee" + testCoffeeBeans = "coffee beans" testCupOfNonsense = "ナンセンス 1杯" testPleaseConsider = "Please consider supporting this project" @@ -468,6 +470,59 @@ func TestDecodeEncode(t *testing.T) { i.Destination = nil }, }, + { + // On mainnet, please send $30 coffee beans supporting + // features 1 and 9. + encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9qzsze992adudgku8p05pstl6zh7av6rx2f297pv89gu5q93a0hf3g7lynl3xq56t23dpvah6u7y9qey9lccrdml3gaqwc6nxsl5ktzm464sq73t7cl", + valid: true, + decodedInvoice: func() *Invoice { + return &Invoice{ + Net: &chaincfg.MainNetParams, + MilliSat: &testMillisat25mBTC, + Timestamp: time.Unix(1496314658, 0), + PaymentHash: &testPaymentHash, + Description: &testCoffeeBeans, + Destination: testPubKey, + Features: lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector(1, 9), + InvoiceFeatures, + ), + } + }, + beforeEncoding: func(i *Invoice) { + // Since this destination pubkey was recovered + // from the signature, we must set it nil before + // encoding to get back the same invoice string. + i.Destination = nil + }, + }, + { + // On mainnet, please send $30 coffee beans supporting + // features 1, 9, and 100. + encodedInvoice: "lnbc25m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5vdhkven9v5sxyetpdees9q4pqqqqqqqqqqqqqqqqqqszk3ed62snp73037h4py4gry05eltlp0uezm2w9ajnerhmxzhzhsu40g9mgyx5v3ad4aqwkmvyftzk4k9zenz90mhjcy9hcevc7r3lx2sphzfxz7", + valid: false, + skipEncoding: true, + decodedInvoice: func() *Invoice { + return &Invoice{ + Net: &chaincfg.MainNetParams, + MilliSat: &testMillisat25mBTC, + Timestamp: time.Unix(1496314658, 0), + PaymentHash: &testPaymentHash, + Description: &testCoffeeBeans, + Destination: testPubKey, + Features: lnwire.NewFeatureVector( + lnwire.NewRawFeatureVector(1, 9, 100), + InvoiceFeatures, + ), + } + }, + beforeEncoding: func(i *Invoice) { + // Since this destination pubkey was recovered + // from the signature, we must set it nil before + // encoding to get back the same invoice string. + i.Destination = nil + }, + }, { // On mainnet, with fallback (p2wpkh) address bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4 encodedInvoice: "lnbc20m1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqhp58yjmdan79s6qqdhdzgynm4zwqd5d7xmw5fk98klysy043l2ahrqsfppqw508d6qejxtdg4y5r3zarvary0c5xw7kknt6zz5vxa8yh8jrnlkl63dah48yh6eupakk87fjdcnwqfcyt7snnpuz7vp83txauq4c60sys3xyucesxjf46yqnpplj0saq36a554cp9wt865", @@ -814,6 +869,11 @@ func compareInvoices(expected, actual *Invoice) error { } } + if !reflect.DeepEqual(expected.Features, actual.Features) { + return fmt.Errorf("expected features %v, got %v", + expected.Features.RawFeatureVector, actual.Features.RawFeatureVector) + } + return nil }