diff --git a/cmd/lncli/commands.go b/cmd/lncli/commands.go index 01a9759d..b21514c4 100644 --- a/cmd/lncli/commands.go +++ b/cmd/lncli/commands.go @@ -1014,6 +1014,7 @@ func sendPayment(ctx *cli.Context) error { if ctx.IsSet("pay_req") { req = &lnrpc.SendRequest{ PaymentRequest: ctx.String("pay_req"), + Amt: ctx.Int64("amt"), } } else { args := ctx.Args() @@ -1138,6 +1139,11 @@ var payInvoiceCommand = cli.Command{ Name: "pay_req", Usage: "a zpay32 encoded payment request to fulfill", }, + cli.Int64Flag{ + Name: "amt", + Usage: "(optional) number of satoshis to fulfill the " + + "invoice", + }, }, Action: actionDecorator(payInvoice), } @@ -1158,6 +1164,7 @@ func payInvoice(ctx *cli.Context) error { req := &lnrpc.SendRequest{ PaymentRequest: payReq, + Amt: ctx.Int64("amt"), } return sendPaymentRequest(ctx, req) @@ -1168,9 +1175,10 @@ var addInvoiceCommand = cli.Command{ Usage: "add a new invoice.", Description: ` Add a new invoice, expressing intent for a future payment. - - The number of satoshis in this invoice is necessary for the creation, - the remaining parameters are optional.`, + + Invoices without an amount can be created by not supplying any + parameters or providing an amount of 0. These invoices allow the payee + to specify the amount of satoshis they wish to send.`, ArgsUsage: "value preimage", Flags: []cli.Flag{ cli.StringFlag{ @@ -1239,8 +1247,6 @@ func addInvoice(ctx *cli.Context) error { if err != nil { return fmt.Errorf("unable to decode amt argument: %v", err) } - default: - return fmt.Errorf("amt argument missing") } switch { diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 1f6ac3b1..0c0d1b66 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -1469,7 +1469,15 @@ func (l *channelLink) processLockedInHtlcs( // Otherwise, we settle this htlc within our // local state update log, then send the update // entry to the remote party. - if !l.cfg.DebugHTLC && pd.Amount < invoice.Terms.Value { + // + // NOTE: We make an exception when the value + // requested by the invoice is zero. This means + // the invoice allows the payee to specify the + // amount of satoshis they wish to send. + // So since we expect the htlc to have a + // different amount, we should not fail. + if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 && + pd.Amount < invoice.Terms.Value { log.Errorf("rejecting htlc due to incorrect "+ "amount: expected %v, received %v", invoice.Terms.Value, pd.Amount) @@ -1484,7 +1492,14 @@ func (l *channelLink) processLockedInHtlcs( // ensure that it was crafted correctly by the // sender and matches the HTLC we were // extended. - if !l.cfg.DebugHTLC && + // + // NOTE: We make an exception when the value + // requested by the invoice is zero. This means + // the invoice allows the payee to specify the + // amount of satoshis they wish to send. + // So since we expect the htlc to have a + // different amount, we should not fail. + if !l.cfg.DebugHTLC && invoice.Terms.Value > 0 && fwdInfo.AmountToForward != invoice.Terms.Value { log.Errorf("Onion payload of incoming "+ diff --git a/rpcserver.go b/rpcserver.go index 560163ac..fa6438d7 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1623,18 +1623,20 @@ func (r *rpcServer) SendPayment(paymentStream lnrpc.Lightning_SendPaymentServer) } p.dest = payReq.Destination.SerializeCompressed() + // If the amount was not included in the + // invoice, then we let the payee + // specify the amount of satoshis they + // wish to send. We override the amount + // to pay with the amount provided from + // the payment request. if payReq.MilliSat == nil { - err := fmt.Errorf("only payment" + - " requests specifying" + - " the amount are" + - " currently supported") - select { - case errChan <- err: - case <-reqQuit: - } - return + p.msat = lnwire.NewMSatFromSatoshis( + btcutil.Amount(nextPayment.Amt), + ) + } else { + p.msat = *payReq.MilliSat } - p.msat = *payReq.MilliSat + p.pHash = payReq.PaymentHash[:] p.cltvDelta = uint16(payReq.MinFinalCLTVExpiry()) } else { @@ -1806,11 +1808,18 @@ func (r *rpcServer) SendPaymentSync(ctx context.Context, destPub = payReq.Destination + // If the amount was not included in the invoice, then we let + // the payee specify the amount of satoshis they wish to send. + // We override the amount to pay with the amount provided from + // the payment request. if payReq.MilliSat == nil { - return nil, fmt.Errorf("payment requests with no " + - "amount specified not currently supported") + amtMSat = lnwire.NewMSatFromSatoshis( + btcutil.Amount(nextPayment.Amt), + ) + } else { + amtMSat = *payReq.MilliSat } - amtMSat = *payReq.MilliSat + rHash = *payReq.PaymentHash cltvDelta = uint16(payReq.MinFinalCLTVExpiry()) @@ -1939,14 +1948,10 @@ func (r *rpcServer) AddInvoice(ctx context.Context, amt := btcutil.Amount(invoice.Value) amtMSat := lnwire.NewMSatFromSatoshis(amt) - switch { - // The value of an invoice MUST NOT be zero. - case invoice.Value == 0: - return nil, fmt.Errorf("zero value invoices are disallowed") // The value of the invoice must also not exceed the current soft-limit // on the largest payment within the network. - case amtMSat > maxPaymentMSat: + if amtMSat > maxPaymentMSat { return nil, fmt.Errorf("payment of %v is too large, max "+ "payment allowed is %v", amt, maxPaymentMSat.ToSatoshis()) } @@ -1962,9 +1967,12 @@ func (r *rpcServer) AddInvoice(ctx context.Context, // expiry, fallback address, and the amount field. var options []func(*zpay32.Invoice) - // Add the amount. This field is optional by the BOLT-11 format, but - // we require it for now. - options = append(options, zpay32.Amount(amtMSat)) + // We only include the amount in the invoice if it is greater than 0. + // By not including the amount, we enable the creation of invoices that + // allow the payee to specify the amount of satoshis they wish to send. + if amtMSat > 0 { + options = append(options, zpay32.Amount(amtMSat)) + } // If specified, add a fallback address to the payment request. if len(invoice.FallbackAddr) > 0 { diff --git a/zpay32/invoice_test.go b/zpay32/invoice_test.go index 036f873d..672253a3 100644 --- a/zpay32/invoice_test.go +++ b/zpay32/invoice_test.go @@ -233,6 +233,26 @@ func TestDecodeEncode(t *testing.T) { }, skipEncoding: true, // Skip encoding since we don't have the unknown fields to encode. }, + { + // Invoice with no amount. + encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jshwlglv23cytkzvq8ld39drs8sq656yh2zn0aevrwu6uqctaklelhtpjnmgjdzmvwsh0kuxuwqf69fjeap9m5mev2qzpp27xfswhs5vgqmn9xzq", + valid: true, + decodedInvoice: func() *Invoice { + return &Invoice{ + Net: &chaincfg.MainNetParams, + Timestamp: time.Unix(1496314658, 0), + PaymentHash: &testPaymentHash, + Description: &testCupOfCoffee, + Destination: testPubKey, + } + }, + 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 + }, + }, { // Please make a donation of any amount using rhash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w", @@ -531,6 +551,19 @@ func TestNewInvoice(t *testing.T) { }, valid: false, // Both Description and DescriptionHash set. }, + { + // Invoice with no amount. + newInvoice: func() (*Invoice, error) { + return NewInvoice( + &chaincfg.MainNetParams, + testPaymentHash, + time.Unix(1496314658, 0), + Description(testCupOfCoffee), + ) + }, + valid: true, + encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jshwlglv23cytkzvq8ld39drs8sq656yh2zn0aevrwu6uqctaklelhtpjnmgjdzmvwsh0kuxuwqf69fjeap9m5mev2qzpp27xfswhs5vgqmn9xzq", + }, { // 'n' field set. newInvoice: func() (*Invoice, error) {