Merge pull request #3788 from cfromknecht/payment-addr
parse and generate bolt 11 invoices w/ payment addrs
This commit is contained in:
8 files changed
+660
-648
No files matched your search
@@ -370,6 +370,15 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||||||
)
|
)
|
||||||
options = append(options, zpay32.Features(invoiceFeatures))
|
options = append(options, zpay32.Features(invoiceFeatures))
|
||||||
|
|
||||||
|
// Generate and set a random payment address for this invoice. If the
|
||||||
|
// sender understands payment addresses, this can be used to avoid
|
||||||
|
// intermediaries probing the receiver.
|
||||||
|
var paymentAddr [32]byte
|
||||||
|
if _, err := rand.Read(paymentAddr[:]); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
options = append(options, zpay32.PaymentAddr(paymentAddr))
|
||||||
|
|
||||||
// Create and encode the payment request as a bech32 (zpay32) string.
|
// Create and encode the payment request as a bech32 (zpay32) string.
|
||||||
creationDate := time.Now()
|
creationDate := time.Now()
|
||||||
payReq, err := zpay32.NewInvoice(
|
payReq, err := zpay32.NewInvoice(
|
||||||
@@ -397,6 +406,7 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
|
|||||||
Expiry: payReq.Expiry(),
|
Expiry: payReq.Expiry(),
|
||||||
Value: amtMSat,
|
Value: amtMSat,
|
||||||
PaymentPreimage: paymentPreimage,
|
PaymentPreimage: paymentPreimage,
|
||||||
|
PaymentAddr: paymentAddr,
|
||||||
Features: invoiceFeatures,
|
Features: invoiceFeatures,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+548
-540
File diff suppressed because it is too large.
Load diff
@@ -2615,6 +2615,7 @@ message PayReq {
|
|||||||
string fallback_addr = 8 [json_name = "fallback_addr"];
|
string fallback_addr = 8 [json_name = "fallback_addr"];
|
||||||
int64 cltv_expiry = 9 [json_name = "cltv_expiry"];
|
int64 cltv_expiry = 9 [json_name = "cltv_expiry"];
|
||||||
repeated RouteHint route_hints = 10 [json_name = "route_hints"];
|
repeated RouteHint route_hints = 10 [json_name = "route_hints"];
|
||||||
|
bytes payment_addr = 11 [json_name = "payment_addr"];
|
||||||
}
|
}
|
||||||
|
|
||||||
message FeeReportRequest {}
|
message FeeReportRequest {}
|
||||||
|
|||||||
@@ -3248,6 +3248,10 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/lnrpcRouteHint"
|
"$ref": "#/definitions/lnrpcRouteHint"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"payment_addr": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "byte"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4593,6 +4593,12 @@ func (r *rpcServer) DecodePayReq(ctx context.Context,
|
|||||||
amt = int64(payReq.MilliSat.ToSatoshis())
|
amt = int64(payReq.MilliSat.ToSatoshis())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the payment address from the payment request, if present.
|
||||||
|
var paymentAddr []byte
|
||||||
|
if payReq.PaymentAddr != nil {
|
||||||
|
paymentAddr = payReq.PaymentAddr[:]
|
||||||
|
}
|
||||||
|
|
||||||
dest := payReq.Destination.SerializeCompressed()
|
dest := payReq.Destination.SerializeCompressed()
|
||||||
return &lnrpc.PayReq{
|
return &lnrpc.PayReq{
|
||||||
Destination: hex.EncodeToString(dest),
|
Destination: hex.EncodeToString(dest),
|
||||||
@@ -4605,6 +4611,7 @@ func (r *rpcServer) DecodePayReq(ctx context.Context,
|
|||||||
Expiry: expiry,
|
Expiry: expiry,
|
||||||
CltvExpiry: int64(payReq.MinFinalCLTVExpiry()),
|
CltvExpiry: int64(payReq.MinFinalCLTVExpiry()),
|
||||||
RouteHints: routeHints,
|
RouteHints: routeHints,
|
||||||
|
PaymentAddr: paymentAddr,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+55
-52
@@ -73,6 +73,11 @@ const (
|
|||||||
// supported or required by the receiver.
|
// supported or required by the receiver.
|
||||||
fieldType9 = 5
|
fieldType9 = 5
|
||||||
|
|
||||||
|
// fieldTypeS contains a 32-byte payment address, which is a nonce
|
||||||
|
// included in the final hop's payload to prevent intermediaries from
|
||||||
|
// probing the recipient.
|
||||||
|
fieldTypeS = 16
|
||||||
|
|
||||||
// maxInvoiceLength is the maximum total length an invoice can have.
|
// maxInvoiceLength is the maximum total length an invoice can have.
|
||||||
// This is chosen to be the maximum number of bytes that can fit into a
|
// This is chosen to be the maximum number of bytes that can fit into a
|
||||||
// single QR code: https://en.wikipedia.org/wiki/QR_code#Storage
|
// single QR code: https://en.wikipedia.org/wiki/QR_code#Storage
|
||||||
@@ -126,6 +131,10 @@ type Invoice struct {
|
|||||||
// invoice.
|
// invoice.
|
||||||
PaymentHash *[32]byte
|
PaymentHash *[32]byte
|
||||||
|
|
||||||
|
// PaymentAddr is the payment address to be used by payments to prevent
|
||||||
|
// probing of the destination.
|
||||||
|
PaymentAddr *[32]byte
|
||||||
|
|
||||||
// Destination is the public key of the target node. This will always
|
// Destination is the public key of the target node. This will always
|
||||||
// be set after decoding, and can optionally be set before encoding to
|
// be set after decoding, and can optionally be set before encoding to
|
||||||
// include the pubkey as an 'n' field. If this is not set before
|
// include the pubkey as an 'n' field. If this is not set before
|
||||||
@@ -258,6 +267,14 @@ func Features(features *lnwire.FeatureVector) func(*Invoice) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PaymentAddr is a functional option that allows callers of NewInvoice to set
|
||||||
|
// the desired payment address tht is advertised on the invoice.
|
||||||
|
func PaymentAddr(addr [32]byte) func(*Invoice) {
|
||||||
|
return func(i *Invoice) {
|
||||||
|
i.PaymentAddr = &addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewInvoice creates a new Invoice object. The last parameter is a set of
|
// NewInvoice creates a new Invoice object. The last parameter is a set of
|
||||||
// variadic arguments for setting optional fields of the invoice.
|
// variadic arguments for setting optional fields of the invoice.
|
||||||
//
|
//
|
||||||
@@ -642,7 +659,15 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice.PaymentHash, err = parsePaymentHash(base32Data)
|
invoice.PaymentHash, err = parse32Bytes(base32Data)
|
||||||
|
case fieldTypeS:
|
||||||
|
if invoice.PaymentAddr != nil {
|
||||||
|
// We skip the field if we have already seen a
|
||||||
|
// supported one.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.PaymentAddr, err = parse32Bytes(base32Data)
|
||||||
case fieldTypeD:
|
case fieldTypeD:
|
||||||
if invoice.Description != nil {
|
if invoice.Description != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -666,7 +691,7 @@ func parseTaggedFields(invoice *Invoice, fields []byte, net *chaincfg.Params) er
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice.DescriptionHash, err = parseDescriptionHash(base32Data)
|
invoice.DescriptionHash, err = parse32Bytes(base32Data)
|
||||||
case fieldTypeX:
|
case fieldTypeX:
|
||||||
if invoice.expiry != nil {
|
if invoice.expiry != nil {
|
||||||
// We skip the field if we have already seen a
|
// We skip the field if we have already seen a
|
||||||
@@ -733,12 +758,12 @@ func parseFieldDataLength(data []byte) (uint16, error) {
|
|||||||
return uint16(data[0])<<5 | uint16(data[1]), nil
|
return uint16(data[0])<<5 | uint16(data[1]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parsePaymentHash converts a 256-bit payment hash (encoded in base32)
|
// parse32Bytes converts a 256-bit value (encoded in base32) to *[32]byte. This
|
||||||
// to *[32]byte.
|
// can be used for payment hashes, description hashes, payment addresses, etc.
|
||||||
func parsePaymentHash(data []byte) (*[32]byte, error) {
|
func parse32Bytes(data []byte) (*[32]byte, error) {
|
||||||
var paymentHash [32]byte
|
var paymentHash [32]byte
|
||||||
|
|
||||||
// As BOLT-11 states, a reader must skip over the payment hash field if
|
// As BOLT-11 states, a reader must skip over the 32-byte fields if
|
||||||
// it does not have a length of 52, so avoid returning an error.
|
// it does not have a length of 52, so avoid returning an error.
|
||||||
if len(data) != hashBase32Len {
|
if len(data) != hashBase32Len {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -784,27 +809,6 @@ func parseDestination(data []byte) (*btcec.PublicKey, error) {
|
|||||||
return btcec.ParsePubKey(base256Data, btcec.S256())
|
return btcec.ParsePubKey(base256Data, btcec.S256())
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseDescriptionHash converts a 256-bit description hash (encoded in base32)
|
|
||||||
// to *[32]byte.
|
|
||||||
func parseDescriptionHash(data []byte) (*[32]byte, error) {
|
|
||||||
var descriptionHash [32]byte
|
|
||||||
|
|
||||||
// As BOLT-11 states, a reader must skip over the description hash field
|
|
||||||
// if it does not have a length of 52, so avoid returning an error.
|
|
||||||
if len(data) != hashBase32Len {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hash, err := bech32.ConvertBits(data, 5, 8, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
copy(descriptionHash[:], hash[:])
|
|
||||||
|
|
||||||
return &descriptionHash, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseExpiry converts the data (encoded in base32) into the expiry time.
|
// parseExpiry converts the data (encoded in base32) into the expiry time.
|
||||||
func parseExpiry(data []byte) (*time.Duration, error) {
|
func parseExpiry(data []byte) (*time.Duration, error) {
|
||||||
expiry, err := base32ToUint64(data)
|
expiry, err := base32ToUint64(data)
|
||||||
@@ -944,18 +948,7 @@ func parseFeatures(data []byte) (*lnwire.FeatureVector, error) {
|
|||||||
// base32 buffer.
|
// base32 buffer.
|
||||||
func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
||||||
if invoice.PaymentHash != nil {
|
if invoice.PaymentHash != nil {
|
||||||
// Convert 32 byte hash to 52 5-bit groups.
|
err := writeBytes32(bufferBase32, fieldTypeP, *invoice.PaymentHash)
|
||||||
base32, err := bech32.ConvertBits(invoice.PaymentHash[:], 8, 5,
|
|
||||||
true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(base32) != hashBase32Len {
|
|
||||||
return fmt.Errorf("invalid payment hash length: %d",
|
|
||||||
len(invoice.PaymentHash))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeTaggedField(bufferBase32, fieldTypeP, base32)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -974,19 +967,9 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if invoice.DescriptionHash != nil {
|
if invoice.DescriptionHash != nil {
|
||||||
// Convert 32 byte hash to 52 5-bit groups.
|
err := writeBytes32(
|
||||||
descBase32, err := bech32.ConvertBits(
|
bufferBase32, fieldTypeH, *invoice.DescriptionHash,
|
||||||
invoice.DescriptionHash[:], 8, 5, true)
|
)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(descBase32) != hashBase32Len {
|
|
||||||
return fmt.Errorf("invalid description hash length: %d",
|
|
||||||
len(invoice.DescriptionHash))
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeTaggedField(bufferBase32, fieldTypeH, descBase32)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1102,10 +1085,30 @@ func writeTaggedFields(bufferBase32 *bytes.Buffer, invoice *Invoice) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if invoice.PaymentAddr != nil {
|
||||||
|
err := writeBytes32(
|
||||||
|
bufferBase32, fieldTypeS, *invoice.PaymentAddr,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeBytes32 encodes a 32-byte array as base32 and writes it to bufferBase32
|
||||||
|
// under the passed fieldType.
|
||||||
|
func writeBytes32(bufferBase32 *bytes.Buffer, fieldType byte, b [32]byte) error {
|
||||||
|
// Convert 32 byte hash to 52 5-bit groups.
|
||||||
|
base32, err := bech32.ConvertBits(b[:], 8, 5, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeTaggedField(bufferBase32, fieldType, base32)
|
||||||
|
}
|
||||||
|
|
||||||
// writeTaggedField takes the type of a tagged data field, and the data of
|
// writeTaggedField takes the type of a tagged data field, and the data of
|
||||||
// the tagged field (encoded in base32), and writes the type, length and data
|
// the tagged field (encoded in base32), and writes the type, length and data
|
||||||
// to the buffer.
|
// to the buffer.
|
||||||
|
|||||||
@@ -314,10 +314,10 @@ func TestParseFieldDataLength(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestParsePaymentHash checks that the payment hash is properly parsed.
|
// TestParse32Bytes checks that the payment hash is properly parsed.
|
||||||
// If the data does not have a length of 52 bytes, we skip over parsing the
|
// If the data does not have a length of 52 bytes, we skip over parsing the
|
||||||
// field and do not return an error.
|
// field and do not return an error.
|
||||||
func TestParsePaymentHash(t *testing.T) {
|
func TestParse32Bytes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
testPaymentHashData, _ := bech32.ConvertBits(testPaymentHash[:], 8, 5, true)
|
testPaymentHashData, _ := bech32.ConvertBits(testPaymentHash[:], 8, 5, true)
|
||||||
@@ -350,7 +350,7 @@ func TestParsePaymentHash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
paymentHash, err := parsePaymentHash(test.data)
|
paymentHash, err := parse32Bytes(test.data)
|
||||||
if (err == nil) != test.valid {
|
if (err == nil) != test.valid {
|
||||||
t.Errorf("payment hash decoding test %d failed: %v", i, err)
|
t.Errorf("payment hash decoding test %d failed: %v", i, err)
|
||||||
return
|
return
|
||||||
@@ -458,56 +458,6 @@ func TestParseDestination(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestParseDescriptionHash checks that the description hash is properly parsed.
|
|
||||||
// If the data does not have a length of 52 bytes, we skip over parsing the
|
|
||||||
// field and do not return an error.
|
|
||||||
func TestParseDescriptionHash(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
testDescriptionHashData, _ := bech32.ConvertBits(testDescriptionHash[:], 8, 5, true)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
data []byte
|
|
||||||
valid bool
|
|
||||||
result *[32]byte
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
data: []byte{},
|
|
||||||
valid: true,
|
|
||||||
result: nil, // skip unknown length, not 52 bytes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: []byte{0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
|
||||||
valid: true,
|
|
||||||
result: nil, // skip unknown length, not 52 bytes
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: testDescriptionHashData,
|
|
||||||
valid: true,
|
|
||||||
result: &testDescriptionHash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: append(testDescriptionHashData, 0x0),
|
|
||||||
valid: true,
|
|
||||||
result: nil, // skip unknown length, not 52 bytes
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, test := range tests {
|
|
||||||
descriptionHash, err := parseDescriptionHash(test.data)
|
|
||||||
if (err == nil) != test.valid {
|
|
||||||
t.Errorf("description hash decoding test %d failed: %v", i, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if test.valid && !compareHashes(descriptionHash, test.result) {
|
|
||||||
t.Fatalf("test %d failed decoding description hash: "+
|
|
||||||
"expected %x, got %x",
|
|
||||||
i, *test.result, *descriptionHash)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestParseExpiry checks that the expiry is properly parsed.
|
// TestParseExpiry checks that the expiry is properly parsed.
|
||||||
func TestParseExpiry(t *testing.T) {
|
func TestParseExpiry(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
+32
-3
@@ -28,7 +28,19 @@ var (
|
|||||||
testMillisat25mBTC = lnwire.MilliSatoshi(2500000000)
|
testMillisat25mBTC = lnwire.MilliSatoshi(2500000000)
|
||||||
testMillisat20mBTC = lnwire.MilliSatoshi(2000000000)
|
testMillisat20mBTC = lnwire.MilliSatoshi(2000000000)
|
||||||
|
|
||||||
testPaymentHashSlice, _ = hex.DecodeString("0001020304050607080900010203040506070809000102030405060708090102")
|
testPaymentHash = [32]byte{
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||||
|
0x08, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||||
|
0x06, 0x07, 0x08, 0x09, 0x00, 0x01, 0x02, 0x03,
|
||||||
|
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x01, 0x02,
|
||||||
|
}
|
||||||
|
|
||||||
|
testPaymentAddr = [32]byte{
|
||||||
|
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x01, 0x02,
|
||||||
|
0x06, 0x07, 0x08, 0x09, 0x00, 0x01, 0x02, 0x03,
|
||||||
|
0x08, 0x09, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||||
|
}
|
||||||
|
|
||||||
testEmptyString = ""
|
testEmptyString = ""
|
||||||
testCupOfCoffee = "1 cup coffee"
|
testCupOfCoffee = "1 cup coffee"
|
||||||
@@ -94,7 +106,6 @@ var (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Must be initialized in init().
|
// Must be initialized in init().
|
||||||
testPaymentHash [32]byte
|
|
||||||
testDescriptionHash [32]byte
|
testDescriptionHash [32]byte
|
||||||
|
|
||||||
ltcTestNetParams chaincfg.Params
|
ltcTestNetParams chaincfg.Params
|
||||||
@@ -102,7 +113,6 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
copy(testPaymentHash[:], testPaymentHashSlice[:])
|
|
||||||
copy(testDescriptionHash[:], testDescriptionHashSlice[:])
|
copy(testDescriptionHash[:], testDescriptionHashSlice[:])
|
||||||
|
|
||||||
// Initialize litecoin testnet and mainnet params by applying key fields
|
// Initialize litecoin testnet and mainnet params by applying key fields
|
||||||
@@ -587,6 +597,25 @@ func TestDecodeEncode(t *testing.T) {
|
|||||||
return i
|
return i
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// Send 2500uBTC for a cup of coffee with a payment
|
||||||
|
// address.
|
||||||
|
encodedInvoice: "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66sp5qszsvpcgpyqsyps8pqysqqgzqvyqjqqpqgpsgpgqqypqxpq9qcrsusq8nx2hdt3st3ankwz23xy9w7udvqq3f0mdlpc6ga5ew3y67u4qkx8vu72ejg5x6tqhyclm28r7r0mg6lx9x3vls9g6glp2qy3y34cpry54xp",
|
||||||
|
valid: true,
|
||||||
|
decodedInvoice: func() *Invoice {
|
||||||
|
i, _ := NewInvoice(
|
||||||
|
&chaincfg.MainNetParams,
|
||||||
|
testPaymentHash,
|
||||||
|
time.Unix(1496314658, 0),
|
||||||
|
Amount(testMillisat2500uBTC),
|
||||||
|
Description(testCupOfCoffee),
|
||||||
|
Destination(testPubKey),
|
||||||
|
PaymentAddr(testPaymentAddr),
|
||||||
|
)
|
||||||
|
|
||||||
|
return i
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// Decode a mainnet invoice while expecting active net to be testnet
|
// Decode a mainnet invoice while expecting active net to be testnet
|
||||||
encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jd3m5klcwhq68vdsmx2rjgxeay5v0tkt2v5sjaky4eqahe4fx3k9sqavvce3capfuwv8rvjng57jrtfajn5dkpqv8yelsewtljwmmycq62k443",
|
encodedInvoice: "lnbc241pveeq09pp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdqqnp4q0n326hr8v9zprg8gsvezcch06gfaqqhde2aj730yg0durunfhv66jd3m5klcwhq68vdsmx2rjgxeay5v0tkt2v5sjaky4eqahe4fx3k9sqavvce3capfuwv8rvjng57jrtfajn5dkpqv8yelsewtljwmmycq62k443",
|
||||||
|
|||||||
Reference in new issue
Block a user