diff --git a/cmd/commands/invoicesrpc_active.go b/cmd/commands/invoicesrpc_active.go index 0ee767c8b2..f854b738ca 100644 --- a/cmd/commands/invoicesrpc_active.go +++ b/cmd/commands/invoicesrpc_active.go @@ -145,12 +145,17 @@ var addHoldInvoiceCommand = cli.Command{ Category: "Invoices", Usage: "Add a new hold invoice.", Description: ` - Add a new invoice, expressing intent for a future payment. + Add a new hold invoice, expressing intent for a future payment. Invoices without an amount can be created by not supplying any parameters or providing an amount of 0. These invoices allow the payer - to specify the amount of satoshis they wish to send.`, - ArgsUsage: "hash [amt]", + to specify the amount of satoshis they wish to send. + + The hash can be provided as the first positional argument (legacy) or + via the --hash flag. If neither hash nor preimage is provided, the + server will auto-generate a random preimage and derive the hash. + When --preimage is provided, the server derives the hash from it.`, + ArgsUsage: "[hash] [amt]", Flags: []cli.Flag{ cli.StringFlag{ Name: "memo", @@ -198,6 +203,20 @@ var addHoldInvoiceCommand = cli.Command{ "private channels in order to assist the " + "payer in reaching you", }, + cli.StringFlag{ + Name: "hash", + Usage: "the hash of the preimage (32 bytes, hex " + + "encoded). If not set, and --preimage is " + + "not set, a random preimage and hash " + + "will be generated.", + }, + cli.StringFlag{ + Name: "preimage", + Usage: "a user-supplied preimage (32 bytes, hex " + + "encoded). The server will derive the " + + "hash from it. Cannot be used together " + + "with --hash.", + }, }, Action: actionDecorator(addHoldInvoice), } @@ -205,6 +224,10 @@ var addHoldInvoiceCommand = cli.Command{ func addHoldInvoice(ctx *cli.Context) error { var ( descHash []byte + hash []byte + preimage []byte + amt int64 + amtMsat int64 err error ) @@ -213,26 +236,48 @@ func addHoldInvoice(ctx *cli.Context) error { defer cleanUp() args := ctx.Args() - if ctx.NArg() == 0 { - cli.ShowCommandHelp(ctx, "addholdinvoice") - return nil - } - hash, err := hex.DecodeString(args.First()) - if err != nil { - return fmt.Errorf("unable to parse hash: %w", err) + switch { + // --hash flag takes priority. + case ctx.IsSet("hash"): + hash, err = hex.DecodeString(ctx.String("hash")) + if err != nil { + return fmt.Errorf("unable to parse hash: %w", err) + } + + // If the first positional arg looks like a hex-encoded hash + // (64 hex chars = 32 bytes), use it for backward compatibility. + case args.Present() && len(args.First()) == 64: + hash, err = hex.DecodeString(args.First()) + if err != nil { + return fmt.Errorf("unable to parse hash: %w", err) + } + args = args.Tail() } - args = args.Tail() + // Parse preimage if provided via flag. + if ctx.IsSet("preimage") { + preimageHex := ctx.String("preimage") + if len(preimageHex) != 64 { + return fmt.Errorf("preimage must be 64 hex " + + "characters (32 bytes)") + } - amt := ctx.Int64("amt") - amtMsat := ctx.Int64("amt_msat") + preimage, err = hex.DecodeString(preimageHex) + if err != nil { + return fmt.Errorf("unable to parse preimage: %w", + err) + } + } + + amt = ctx.Int64("amt") + amtMsat = ctx.Int64("amt_msat") if !ctx.IsSet("amt") && !ctx.IsSet("amt_msat") && args.Present() { amt, err = strconv.ParseInt(args.First(), 10, 64) if err != nil { - return fmt.Errorf("unable to decode amt argument: %w", - err) + return fmt.Errorf("unable to decode amt argument: "+ + "%w", err) } } @@ -251,6 +296,7 @@ func addHoldInvoice(ctx *cli.Context) error { Expiry: ctx.Int64("expiry"), CltvExpiry: ctx.Uint64("cltv_expiry_delta"), Private: ctx.Bool("private"), + Preimage: preimage, } resp, err := client.AddHoldInvoice(ctxc, invoice) diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md new file mode 100644 index 0000000000..fa4afc07f5 --- /dev/null +++ b/docs/release-notes/release-notes-0.22.0.md @@ -0,0 +1,75 @@ +# Release Notes +- [Bug Fixes](#bug-fixes) +- [New Features](#new-features) + - [Functional Enhancements](#functional-enhancements) + - [RPC Additions](#rpc-additions) + - [lncli Additions](#lncli-additions) +- [Improvements](#improvements) + - [Functional Updates](#functional-updates) + - [RPC Updates](#rpc-updates) + - [lncli Updates](#lncli-updates) + - [Breaking Changes](#breaking-changes) + - [Performance Improvements](#performance-improvements) + - [Deprecations](#deprecations) +- [Technical and Architectural Updates](#technical-and-architectural-updates) + - [BOLT Spec Updates](#bolt-spec-updates) + - [Testing](#testing) + - [Database](#database) + - [Code Health](#code-health) + - [Tooling and Documentation](#tooling-and-documentation) + +# Bug Fixes + +# New Features + +## Functional Enhancements + +## RPC Additions + +## lncli Additions + +# Improvements + +## Functional Updates + +## RPC Updates + +* [`AddHoldInvoice` now supports optional preimage/hash + generation](https://github.com/lightningnetwork/lnd/pull/10685). The `hash` + field is no longer required — when omitted, the server auto-generates a + cryptographically random preimage and derives the payment hash. A new + `preimage` field on the request allows callers to supply their own preimage + and let the server derive the hash, eliminating the risk of hash/preimage + mismatches. The response now includes `payment_preimage` (when the server + knows the preimage) and `payment_hash` (always populated). + +## lncli Updates + +* The [`addholdinvoice` command now accepts `--hash` and `--preimage` + flags](https://github.com/lightningnetwork/lnd/pull/10685). When neither is + provided, the server generates both automatically. The legacy positional hash + argument is still supported for backward compatibility. + +## Code Health + +## Breaking Changes + +## Performance Improvements + +## Deprecations + +# Technical and Architectural Updates + +## BOLT Spec Updates + +## Testing + +## Database + +## Code Health + +## Tooling and Documentation + +# Contributors (Alphabetical Order) + +* Suheb diff --git a/itest/list_on_test.go b/itest/list_on_test.go index 3e6bc9f8b8..d3c1e8d457 100644 --- a/itest/list_on_test.go +++ b/itest/list_on_test.go @@ -250,6 +250,10 @@ var allTestCases = []*lntest.TestCase{ Name: "hold invoice sender persistence", TestFunc: testHoldInvoicePersistence, }, + { + Name: "hold invoice auto generate", + TestFunc: testHoldInvoiceAutoGenerate, + }, { Name: "maximum channel size", TestFunc: testMaxChannelSize, diff --git a/itest/lnd_hold_invoice_auto_generate_test.go b/itest/lnd_hold_invoice_auto_generate_test.go new file mode 100644 index 0000000000..3d8d8ee9aa --- /dev/null +++ b/itest/lnd_hold_invoice_auto_generate_test.go @@ -0,0 +1,155 @@ +package itest + +import ( + "context" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" + "github.com/lightningnetwork/lnd/lnrpc/routerrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" +) + +// testHoldInvoiceAutoGenerate tests that hold invoices can be created without +// providing a hash, with the server auto-generating a preimage and hash. It +// also tests the case where a preimage is provided and the server derives the +// hash, and verifies that both hash and preimage set returns an error. +func testHoldInvoiceAutoGenerate(ht *lntest.HarnessTest) { + // Open a channel between Alice and Bob. + _, nodes := ht.CreateSimpleNetwork( + [][]string{nil, nil}, lntest.OpenChannelParams{ + Amt: btcutil.Amount(1_000_000), + }, + ) + alice, bob := nodes[0], nodes[1] + + // Test 1: Auto-generate preimage and hash (neither provided). + autoReq := &invoicesrpc.AddHoldInvoiceRequest{ + Memo: "auto-generated", + Value: 10_000, + } + autoResp := bob.RPC.AddHoldInvoice(autoReq) + + // The response must contain both a preimage and hash. + require.Len(ht, autoResp.PaymentPreimage, 32, + "expected 32-byte preimage") + require.Len(ht, autoResp.PaymentHash, 32, + "expected 32-byte payment hash") + + // Verify the hash matches SHA256 of the preimage. + var autoPreimage lntypes.Preimage + copy(autoPreimage[:], autoResp.PaymentPreimage) + autoHash := autoPreimage.Hash() + require.Equal(ht, autoHash[:], autoResp.PaymentHash, + "hash should be SHA256 of preimage") + + // Subscribe, pay, and settle. + autoStream := bob.RPC.SubscribeSingleInvoice(autoResp.PaymentHash) + + ht.SendPaymentAndAssertStatus(alice, &routerrpc.SendPaymentRequest{ + PaymentRequest: autoResp.PaymentRequest, + FeeLimitSat: 1_000_000, + }, lnrpc.Payment_IN_FLIGHT) + + ht.AssertInvoiceState(autoStream, lnrpc.Invoice_ACCEPTED) + + bob.RPC.SettleInvoice(autoResp.PaymentPreimage) + ht.AssertInvoiceState(autoStream, lnrpc.Invoice_SETTLED) + ht.AssertPaymentStatus( + alice, autoHash, lnrpc.Payment_SUCCEEDED, + ) + + // Test 2: User-supplied preimage (server derives hash). + var userPreimage lntypes.Preimage + copy(userPreimage[:], ht.Random32Bytes()) + expectedHash := userPreimage.Hash() + + preimgResp := bob.RPC.AddHoldInvoice( + &invoicesrpc.AddHoldInvoiceRequest{ + Memo: "user-preimage", + Value: 10_000, + Preimage: userPreimage[:], + }, + ) + + // The response should echo back the preimage and correct hash. + require.Equal(ht, userPreimage[:], preimgResp.PaymentPreimage, + "preimage should be echoed back") + require.Equal(ht, expectedHash[:], preimgResp.PaymentHash, + "hash should match SHA256 of preimage") + + // Subscribe, pay, and settle. + preimgStream := bob.RPC.SubscribeSingleInvoice( + preimgResp.PaymentHash, + ) + + ht.SendPaymentAndAssertStatus(alice, &routerrpc.SendPaymentRequest{ + PaymentRequest: preimgResp.PaymentRequest, + FeeLimitSat: 1_000_000, + }, lnrpc.Payment_IN_FLIGHT) + + ht.AssertInvoiceState(preimgStream, lnrpc.Invoice_ACCEPTED) + + bob.RPC.SettleInvoice(userPreimage[:]) + ht.AssertInvoiceState(preimgStream, lnrpc.Invoice_SETTLED) + ht.AssertPaymentStatus( + alice, expectedHash, lnrpc.Payment_SUCCEEDED, + ) + + // Test 3: Traditional hash-only flow still works. + var hashPreimage lntypes.Preimage + copy(hashPreimage[:], ht.Random32Bytes()) + payHash := hashPreimage.Hash() + + hashResp := bob.RPC.AddHoldInvoice( + &invoicesrpc.AddHoldInvoiceRequest{ + Memo: "hash-only", + Value: 10_000, + Hash: payHash[:], + }, + ) + + // Preimage should be empty since the server doesn't know it. + require.Empty(ht, hashResp.PaymentPreimage, + "preimage should be empty for hash-only") + + // Hash should match what we provided. + require.Equal(ht, payHash[:], hashResp.PaymentHash, + "returned hash should match provided hash") + + // Subscribe, pay, and settle. + hashStream := bob.RPC.SubscribeSingleInvoice(payHash[:]) + + ht.SendPaymentAndAssertStatus(alice, &routerrpc.SendPaymentRequest{ + PaymentRequest: hashResp.PaymentRequest, + FeeLimitSat: 1_000_000, + }, lnrpc.Payment_IN_FLIGHT) + + ht.AssertInvoiceState(hashStream, lnrpc.Invoice_ACCEPTED) + + bob.RPC.SettleInvoice(hashPreimage[:]) + ht.AssertInvoiceState(hashStream, lnrpc.Invoice_SETTLED) + ht.AssertPaymentStatus( + alice, payHash, lnrpc.Payment_SUCCEEDED, + ) + + // Test 4: Both hash and preimage should error. + var bothPreimage lntypes.Preimage + copy(bothPreimage[:], ht.Random32Bytes()) + bothHash := bothPreimage.Hash() + + _, err := bob.RPC.Invoice.AddHoldInvoice( + context.Background(), &invoicesrpc.AddHoldInvoiceRequest{ + Memo: "should-fail", + Value: 10_000, + Hash: bothHash[:], + Preimage: bothPreimage[:], + }, + ) + require.Error(ht, err, "expected error when both hash "+ + "and preimage are set") + require.Contains(ht, err.Error(), + "cannot set both hash and preimage") +} diff --git a/lnrpc/invoicesrpc/invoices.pb.go b/lnrpc/invoicesrpc/invoices.pb.go index e120269988..1d0ee522cc 100644 --- a/lnrpc/invoicesrpc/invoices.pb.go +++ b/lnrpc/invoicesrpc/invoices.pb.go @@ -167,7 +167,9 @@ type AddHoldInvoiceRequest struct { // field of the encoded payment request if the description_hash field is not // being used. Memo string `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"` - // The hash of the preimage + // The hash of the preimage. When creating a hold invoice, the hash is + // optional. If neither hash nor preimage is provided, the server will + // generate a random preimage and derive the hash automatically. Hash []byte `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` // The value of this invoice in satoshis // @@ -191,7 +193,13 @@ type AddHoldInvoiceRequest struct { // invoice's destination. RouteHints []*lnrpc.RouteHint `protobuf:"bytes,8,rep,name=route_hints,json=routeHints,proto3" json:"route_hints,omitempty"` // Whether this invoice should include routing hints for private channels. - Private bool `protobuf:"varint,9,opt,name=private,proto3" json:"private,omitempty"` + Private bool `protobuf:"varint,9,opt,name=private,proto3" json:"private,omitempty"` + // An optional preimage (32 bytes). If provided, the server will validate + // its length and derive the payment hash from it, ignoring the hash field. + // If neither preimage nor hash is provided, the server generates a random + // preimage and derives the hash automatically. This cannot be set at the + // same time as hash. + Preimage []byte `protobuf:"bytes,11,opt,name=preimage,proto3" json:"preimage,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -296,6 +304,13 @@ func (x *AddHoldInvoiceRequest) GetPrivate() bool { return false } +func (x *AddHoldInvoiceRequest) GetPreimage() []byte { + if x != nil { + return x.Preimage + } + return nil +} + type AddHoldInvoiceResp struct { state protoimpl.MessageState `protogen:"open.v1"` // A bare-bones invoice for a payment within the Lightning Network. With the @@ -311,7 +326,15 @@ type AddHoldInvoiceResp struct { // the payment secret in specifications (e.g. BOLT 11). This value should // be used in all payments for this invoice as we require it for end to end // security. - PaymentAddr []byte `protobuf:"bytes,3,opt,name=payment_addr,json=paymentAddr,proto3" json:"payment_addr,omitempty"` + PaymentAddr []byte `protobuf:"bytes,3,opt,name=payment_addr,json=paymentAddr,proto3" json:"payment_addr,omitempty"` + // The preimage for this invoice. This is populated when the server + // generated the preimage (neither hash nor preimage was provided in + // the request) or when the caller provided a preimage. It is empty + // when only a hash was provided, since the server does not know the + // preimage in that case. + PaymentPreimage []byte `protobuf:"bytes,4,opt,name=payment_preimage,json=paymentPreimage,proto3" json:"payment_preimage,omitempty"` + // The payment hash for this invoice. Always populated. + PaymentHash []byte `protobuf:"bytes,5,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -367,6 +390,20 @@ func (x *AddHoldInvoiceResp) GetPaymentAddr() []byte { return nil } +func (x *AddHoldInvoiceResp) GetPaymentPreimage() []byte { + if x != nil { + return x.PaymentPreimage + } + return nil +} + +func (x *AddHoldInvoiceResp) GetPaymentHash() []byte { + if x != nil { + return x.PaymentHash + } + return nil +} + type SettleInvoiceMsg struct { state protoimpl.MessageState `protogen:"open.v1"` // Externally discovered pre-image that should be used to settle the hold @@ -825,7 +862,7 @@ const file_invoicesrpc_invoices_proto_rawDesc = "" + "\x1ainvoicesrpc/invoices.proto\x12\vinvoicesrpc\x1a\x0flightning.proto\"5\n" + "\x10CancelInvoiceMsg\x12!\n" + "\fpayment_hash\x18\x01 \x01(\fR\vpaymentHash\"\x13\n" + - "\x11CancelInvoiceResp\"\xca\x02\n" + + "\x11CancelInvoiceResp\"\xe6\x02\n" + "\x15AddHoldInvoiceRequest\x12\x12\n" + "\x04memo\x18\x01 \x01(\tR\x04memo\x12\x12\n" + "\x04hash\x18\x02 \x01(\fR\x04hash\x12\x14\n" + @@ -840,11 +877,14 @@ const file_invoicesrpc_invoices_proto_rawDesc = "" + "cltvExpiry\x121\n" + "\vroute_hints\x18\b \x03(\v2\x10.lnrpc.RouteHintR\n" + "routeHints\x12\x18\n" + - "\aprivate\x18\t \x01(\bR\aprivate\"}\n" + + "\aprivate\x18\t \x01(\bR\aprivate\x12\x1a\n" + + "\bpreimage\x18\v \x01(\fR\bpreimage\"\xcb\x01\n" + "\x12AddHoldInvoiceResp\x12'\n" + "\x0fpayment_request\x18\x01 \x01(\tR\x0epaymentRequest\x12\x1b\n" + "\tadd_index\x18\x02 \x01(\x04R\baddIndex\x12!\n" + - "\fpayment_addr\x18\x03 \x01(\fR\vpaymentAddr\".\n" + + "\fpayment_addr\x18\x03 \x01(\fR\vpaymentAddr\x12)\n" + + "\x10payment_preimage\x18\x04 \x01(\fR\x0fpaymentPreimage\x12!\n" + + "\fpayment_hash\x18\x05 \x01(\fR\vpaymentHash\".\n" + "\x10SettleInvoiceMsg\x12\x1a\n" + "\bpreimage\x18\x01 \x01(\fR\bpreimage\"\x13\n" + "\x11SettleInvoiceResp\"<\n" + diff --git a/lnrpc/invoicesrpc/invoices.proto b/lnrpc/invoicesrpc/invoices.proto index d9d3bc1236..0848960bb1 100644 --- a/lnrpc/invoicesrpc/invoices.proto +++ b/lnrpc/invoicesrpc/invoices.proto @@ -87,7 +87,11 @@ message AddHoldInvoiceRequest { */ string memo = 1; - // The hash of the preimage + /* + The hash of the preimage. When creating a hold invoice, the hash is + optional. If neither hash nor preimage is provided, the server will + generate a random preimage and derive the hash automatically. + */ bytes hash = 2; /* @@ -128,6 +132,15 @@ message AddHoldInvoiceRequest { // Whether this invoice should include routing hints for private channels. bool private = 9; + + /* + An optional preimage (32 bytes). If provided, the server will validate + its length and derive the payment hash from it, ignoring the hash field. + If neither preimage nor hash is provided, the server generates a random + preimage and derives the hash automatically. This cannot be set at the + same time as hash. + */ + bytes preimage = 11; } message AddHoldInvoiceResp { @@ -153,6 +166,20 @@ message AddHoldInvoiceResp { security. */ bytes payment_addr = 3; + + /* + The preimage for this invoice. This is populated when the server + generated the preimage (neither hash nor preimage was provided in + the request) or when the caller provided a preimage. It is empty + when only a hash was provided, since the server does not know the + preimage in that case. + */ + bytes payment_preimage = 4; + + /* + The payment hash for this invoice. Always populated. + */ + bytes payment_hash = 5; } message SettleInvoiceMsg { diff --git a/lnrpc/invoicesrpc/invoices.swagger.json b/lnrpc/invoicesrpc/invoices.swagger.json index a2ca66ec51..b5ced871e0 100644 --- a/lnrpc/invoicesrpc/invoices.swagger.json +++ b/lnrpc/invoicesrpc/invoices.swagger.json @@ -282,7 +282,7 @@ "hash": { "type": "string", "format": "byte", - "title": "The hash of the preimage" + "description": "The hash of the preimage. When creating a hold invoice, the hash is\noptional. If neither hash nor preimage is provided, the server will\ngenerate a random preimage and derive the hash automatically." }, "value": { "type": "string", @@ -326,6 +326,11 @@ "private": { "type": "boolean", "description": "Whether this invoice should include routing hints for private channels." + }, + "preimage": { + "type": "string", + "format": "byte", + "description": "An optional preimage (32 bytes). If provided, the server will validate\nits length and derive the payment hash from it, ignoring the hash field.\nIf neither preimage nor hash is provided, the server generates a random\npreimage and derives the hash automatically. This cannot be set at the\nsame time as hash." } } }, @@ -345,6 +350,16 @@ "type": "string", "format": "byte", "description": "The payment address of the generated invoice. This is also called\nthe payment secret in specifications (e.g. BOLT 11). This value should\nbe used in all payments for this invoice as we require it for end to end\nsecurity." + }, + "payment_preimage": { + "type": "string", + "format": "byte", + "description": "The preimage for this invoice. This is populated when the server\ngenerated the preimage (neither hash nor preimage was provided in\nthe request) or when the caller provided a preimage. It is empty\nwhen only a hash was provided, since the server does not know the\npreimage in that case." + }, + "payment_hash": { + "type": "string", + "format": "byte", + "description": "The payment hash for this invoice. Always populated." } } }, diff --git a/lnrpc/invoicesrpc/invoices_server.go b/lnrpc/invoicesrpc/invoices_server.go index 7b00b9b643..ea1dc3d9c3 100644 --- a/lnrpc/invoicesrpc/invoices_server.go +++ b/lnrpc/invoicesrpc/invoices_server.go @@ -352,9 +352,38 @@ func (s *Server) AddHoldInvoice(ctx context.Context, GetAlias: s.cfg.GetAlias, } - hash, err := lntypes.MakeHash(invoice.Hash) - if err != nil { - return nil, err + var ( + hashPtr *lntypes.Hash + preimgPtr *lntypes.Preimage + hasHash = len(invoice.Hash) > 0 + hasPreimg = len(invoice.Preimage) > 0 + ) + + switch { + // Both hash and preimage provided is not allowed. + case hasHash && hasPreimg: + return nil, fmt.Errorf("cannot set both hash and " + + "preimage at the same time") + + // Hash provided: existing behavior. + case hasHash: + hash, err := lntypes.MakeHash(invoice.Hash) + if err != nil { + return nil, fmt.Errorf("invalid hash: %w", err) + } + hashPtr = &hash + + // Preimage provided: validate and derive hash. + case hasPreimg: + preimage, err := lntypes.MakePreimage(invoice.Preimage) + if err != nil { + return nil, fmt.Errorf("invalid preimage: %w", err) + } + preimgPtr = &preimage + + // Neither provided: auto-generate. AddInvoice handles this + // when both Hash and Preimage are nil. + default: } value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat) @@ -367,9 +396,10 @@ func (s *Server) AddHoldInvoice(ctx context.Context, if err != nil { return nil, err } + addInvoiceData := &AddInvoiceData{ Memo: invoice.Memo, - Hash: &hash, + Hash: hashPtr, Value: value, DescriptionHash: invoice.DescriptionHash, Expiry: invoice.Expiry, @@ -377,20 +407,31 @@ func (s *Server) AddHoldInvoice(ctx context.Context, CltvExpiry: invoice.CltvExpiry, Private: invoice.Private, HodlInvoice: true, - Preimage: nil, + Preimage: preimgPtr, RouteHints: routeHints, } - _, dbInvoice, err := AddInvoice(ctx, addInvoiceCfg, addInvoiceData) + payHash, dbInvoice, err := AddInvoice( + ctx, addInvoiceCfg, addInvoiceData, + ) if err != nil { return nil, err } - return &AddHoldInvoiceResp{ + resp := &AddHoldInvoiceResp{ AddIndex: dbInvoice.AddIndex, PaymentRequest: string(dbInvoice.PaymentRequest), PaymentAddr: dbInvoice.Terms.PaymentAddr[:], - }, nil + PaymentHash: payHash[:], + } + + // Return the preimage whenever the server knows it (auto-generated + // or caller-provided preimage cases). + if dbInvoice.Terms.PaymentPreimage != nil { + resp.PaymentPreimage = dbInvoice.Terms.PaymentPreimage[:] + } + + return resp, nil } // LookupInvoiceV2 attempts to look up at invoice. An invoice can be referenced