Merge pull request #654 from wilmerpaulino/invoice-without-amount

Support invoices without amount specified
This commit is contained in:
Olaoluwa Osuntokun 2018-01-27 17:38:56 -08:00 committed by GitHub
commit 7919c4cba0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 28 deletions

@ -1014,6 +1014,7 @@ func sendPayment(ctx *cli.Context) error {
if ctx.IsSet("pay_req") { if ctx.IsSet("pay_req") {
req = &lnrpc.SendRequest{ req = &lnrpc.SendRequest{
PaymentRequest: ctx.String("pay_req"), PaymentRequest: ctx.String("pay_req"),
Amt: ctx.Int64("amt"),
} }
} else { } else {
args := ctx.Args() args := ctx.Args()
@ -1138,6 +1139,11 @@ var payInvoiceCommand = cli.Command{
Name: "pay_req", Name: "pay_req",
Usage: "a zpay32 encoded payment request to fulfill", Usage: "a zpay32 encoded payment request to fulfill",
}, },
cli.Int64Flag{
Name: "amt",
Usage: "(optional) number of satoshis to fulfill the " +
"invoice",
},
}, },
Action: actionDecorator(payInvoice), Action: actionDecorator(payInvoice),
} }
@ -1158,6 +1164,7 @@ func payInvoice(ctx *cli.Context) error {
req := &lnrpc.SendRequest{ req := &lnrpc.SendRequest{
PaymentRequest: payReq, PaymentRequest: payReq,
Amt: ctx.Int64("amt"),
} }
return sendPaymentRequest(ctx, req) return sendPaymentRequest(ctx, req)
@ -1168,9 +1175,10 @@ var addInvoiceCommand = cli.Command{
Usage: "add a new invoice.", Usage: "add a new invoice.",
Description: ` Description: `
Add a new invoice, expressing intent for a future payment. Add a new invoice, expressing intent for a future payment.
The number of satoshis in this invoice is necessary for the creation, Invoices without an amount can be created by not supplying any
the remaining parameters are optional.`, 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", ArgsUsage: "value preimage",
Flags: []cli.Flag{ Flags: []cli.Flag{
cli.StringFlag{ cli.StringFlag{
@ -1239,8 +1247,6 @@ func addInvoice(ctx *cli.Context) error {
if err != nil { if err != nil {
return fmt.Errorf("unable to decode amt argument: %v", err) return fmt.Errorf("unable to decode amt argument: %v", err)
} }
default:
return fmt.Errorf("amt argument missing")
} }
switch { switch {

@ -1469,7 +1469,15 @@ func (l *channelLink) processLockedInHtlcs(
// Otherwise, we settle this htlc within our // Otherwise, we settle this htlc within our
// local state update log, then send the update // local state update log, then send the update
// entry to the remote party. // 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 "+ log.Errorf("rejecting htlc due to incorrect "+
"amount: expected %v, received %v", "amount: expected %v, received %v",
invoice.Terms.Value, pd.Amount) invoice.Terms.Value, pd.Amount)
@ -1484,7 +1492,14 @@ func (l *channelLink) processLockedInHtlcs(
// ensure that it was crafted correctly by the // ensure that it was crafted correctly by the
// sender and matches the HTLC we were // sender and matches the HTLC we were
// extended. // 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 { fwdInfo.AmountToForward != invoice.Terms.Value {
log.Errorf("Onion payload of incoming "+ log.Errorf("Onion payload of incoming "+

@ -1623,18 +1623,20 @@ func (r *rpcServer) SendPayment(paymentStream lnrpc.Lightning_SendPaymentServer)
} }
p.dest = payReq.Destination.SerializeCompressed() 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 { if payReq.MilliSat == nil {
err := fmt.Errorf("only payment" + p.msat = lnwire.NewMSatFromSatoshis(
" requests specifying" + btcutil.Amount(nextPayment.Amt),
" the amount are" + )
" currently supported") } else {
select { p.msat = *payReq.MilliSat
case errChan <- err:
case <-reqQuit:
}
return
} }
p.msat = *payReq.MilliSat
p.pHash = payReq.PaymentHash[:] p.pHash = payReq.PaymentHash[:]
p.cltvDelta = uint16(payReq.MinFinalCLTVExpiry()) p.cltvDelta = uint16(payReq.MinFinalCLTVExpiry())
} else { } else {
@ -1806,11 +1808,18 @@ func (r *rpcServer) SendPaymentSync(ctx context.Context,
destPub = payReq.Destination 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 { if payReq.MilliSat == nil {
return nil, fmt.Errorf("payment requests with no " + amtMSat = lnwire.NewMSatFromSatoshis(
"amount specified not currently supported") btcutil.Amount(nextPayment.Amt),
)
} else {
amtMSat = *payReq.MilliSat
} }
amtMSat = *payReq.MilliSat
rHash = *payReq.PaymentHash rHash = *payReq.PaymentHash
cltvDelta = uint16(payReq.MinFinalCLTVExpiry()) cltvDelta = uint16(payReq.MinFinalCLTVExpiry())
@ -1939,14 +1948,10 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
amt := btcutil.Amount(invoice.Value) amt := btcutil.Amount(invoice.Value)
amtMSat := lnwire.NewMSatFromSatoshis(amt) 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 // The value of the invoice must also not exceed the current soft-limit
// on the largest payment within the network. // on the largest payment within the network.
case amtMSat > maxPaymentMSat: if amtMSat > maxPaymentMSat {
return nil, fmt.Errorf("payment of %v is too large, max "+ return nil, fmt.Errorf("payment of %v is too large, max "+
"payment allowed is %v", amt, maxPaymentMSat.ToSatoshis()) "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. // expiry, fallback address, and the amount field.
var options []func(*zpay32.Invoice) var options []func(*zpay32.Invoice)
// Add the amount. This field is optional by the BOLT-11 format, but // We only include the amount in the invoice if it is greater than 0.
// we require it for now. // By not including the amount, we enable the creation of invoices that
options = append(options, zpay32.Amount(amtMSat)) // 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 specified, add a fallback address to the payment request.
if len(invoice.FallbackAddr) > 0 { if len(invoice.FallbackAddr) > 0 {

@ -233,6 +233,26 @@ func TestDecodeEncode(t *testing.T) {
}, },
skipEncoding: true, // Skip encoding since we don't have the unknown fields to encode. 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 // Please make a donation of any amount using rhash 0001020304050607080900010203040506070809000102030405060708090102 to me @03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad
encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w", encodedInvoice: "lnbc1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdpl2pkx2ctnv5sxxmmwwd5kgetjypeh2ursdae8g6twvus8g6rfwvs8qun0dfjkxaq8rkx3yf5tcsyz3d73gafnh3cax9rn449d9p5uxz9ezhhypd0elx87sjle52x86fux2ypatgddc6k63n7erqz25le42c4u4ecky03ylcqca784w",
@ -531,6 +551,19 @@ func TestNewInvoice(t *testing.T) {
}, },
valid: false, // Both Description and DescriptionHash set. 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. // 'n' field set.
newInvoice: func() (*Invoice, error) { newInvoice: func() (*Invoice, error) {