Browse Source

offers: split offer send_invoice generation into new JSON command

We split `send_invoice` offers inoo offerout (for want of a better name).
This simplifies the API.

Also took the opportunity to move the `vendor` tag to immediately
follow `description` (our tests use arguments by keywords, so no
change there).

Suggested-by: shesek
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
ppa
Rusty Russell 4 years ago
parent
commit
1aa7e8e011
  1. 1
      doc/Makefile
  2. 1
      doc/index.rst
  3. 30
      doc/lightning-offer.7
  4. 27
      doc/lightning-offer.7.md
  5. 113
      doc/lightning-offerout.7
  6. 93
      doc/lightning-offerout.7.md
  7. 11
      plugins/offers.c
  8. 124
      plugins/offers_offer.c
  9. 4
      plugins/offers_offer.h
  10. 11
      tests/test_pay.py

1
doc/Makefile

@ -44,6 +44,7 @@ MANPAGES := doc/lightning-cli.1 \
doc/lightning-newaddr.7 \
doc/lightning-notifications.7 \
doc/lightning-offer.7 \
doc/lightning-offerout.7 \
doc/lightning-openchannel_init.7 \
doc/lightning-openchannel_signed.7 \
doc/lightning-openchannel_update.7 \

1
doc/index.rst

@ -73,6 +73,7 @@ c-lightning Documentation
lightning-newaddr <lightning-newaddr.7.md>
lightning-notifications <lightning-notifications.7.md>
lightning-offer <lightning-offer.7.md>
lightning-offerout <lightning-offerout.7.md>
lightning-openchannel_init <lightning-openchannel_init.7.md>
lightning-openchannel_signed <lightning-openchannel_signed.7.md>
lightning-openchannel_update <lightning-openchannel_update.7.md>

30
doc/lightning-offer.7

@ -6,14 +6,13 @@ lightning-offer - Command for accepting payments
\fIEXPERIMENTAL_FEATURES only\fR
\fBoffer\fR \fIamount\fR \fIdescription\fR [\fIsend_invoice\fR] [\fIlabel\fR] [\fIvendor\fR] [\fIquantity_min\fR] [\fIquantity_max\fR] [\fIabsolute_expiry\fR] [\fIrecurrence\fR] [\fIrecurrence_base\fR] [\fIrecurrence_paywindow\fR] [\fIrecurrence_limit\fR] [\fIrefund_for\fR] [\fIsingle_use\fR]
\fBoffer\fR \fIamount\fR \fIdescription\fR [\fIvendor\fR] [\fIlabel\fR] [\fIquantity_min\fR] [\fIquantity_max\fR] [\fIabsolute_expiry\fR] [\fIrecurrence\fR] [\fIrecurrence_base\fR] [\fIrecurrence_paywindow\fR] [\fIrecurrence_limit\fR] [\fIsingle_use\fR]
.SH DESCRIPTION
The \fBoffer\fR RPC command creates an offer, which is a precursor to
one or more invoices\. It automatically enables the accepting of
corresponding invoice_request or invoice messages (depending on
\fIsend_invoice\fR)\.
creating one or more invoices\. It automatically enables the processing of
an incoming invoice_request, and issuing of invoices\.
The \fIamount\fR parameter can be the string "any", which creates an offer
@ -38,13 +37,6 @@ The \fIvendor\fR is another (optional) field exposed in the offer, and
reflects who is issuing this offer (i\.e\. you) if appropriate\.
The \fIsend_invoice\fR boolean (default false unless \fIsingle_use\fR) creates
an offer to send money: the user of the offer will send an invoice,
rather than an invoice_request\. This is encoded in the offer\. Note
that \fIrecurrence\fR and ISO 4217 currencies are not currently
well-supported for this case!
The \fIlabel\fR field is an internal-use name for the offer, which can
be any UTF-8 string\.
@ -100,13 +92,9 @@ This implies \fIsend_invoice\fR and \fIsingle_use\fR\. This is encoded in the
offer\.
\fIsingle_use\fR (default false, unless \fIrefund_for\fR) indicates that the
invoice associated with the offer is only valid once; for a
\fIsend_invoice\fR offer many invoices can be accepted until one is
successfully paid (and we will only attempt to pay one at any time)\.
For a non-\fIsingle-use\fR offer, we will issue any number of invoices as
requested, until one is paid, at which time we will expire all the
other invoices for this offer and issue no more\.
\fIsingle_use\fR (default false) indicates that the offer is only valid
once; we may issue multiple invoices, but as soon as one is paid all other
invoices will be expired (i\.e\. only one person can pay this offer)\.
.SH RETURN VALUE
@ -118,7 +106,7 @@ On success, an object as follows is returned:
.IP \[bu]
\fIactive\fR: true
.IP \[bu]
\fIsingle_use\fR: true if \fIsingle_use\fR was specified or implied\.
\fIsingle_use\fR: true if \fIsingle_use\fR was specified\.
.IP \[bu]
\fIbolt12\fR: the bolt12 offer, starting with "lno1"
@ -153,10 +141,10 @@ Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
.SH SEE ALSO
\fBlightning-listoffers\fR(7), \fBlightning-deloffer\fR(7)\.
\fBlightning-offerout\fR(7), \fBlightning-listoffers\fR(7), \fBlightning-deloffer\fR(7)\.
.SH RESOURCES
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:88a1e0515adae79cdeef661b6462879d7fb0d666a7731ffdb131053c15c1b9de
\" SHA256STAMP:54947f0571c064b5190b672f79dd8c4b4555aad3e93007c28deab37c9a0566c1

27
doc/lightning-offer.7.md

@ -6,15 +6,14 @@ SYNOPSIS
*EXPERIMENTAL_FEATURES only*
**offer** *amount* *description* \[*send_invoice*\] \[*label*\] \[*vendor*\] \[*quantity_min*\] \[*quantity_max*\] \[*absolute_expiry*\] \[*recurrence*\] \[*recurrence_base*\] \[*recurrence_paywindow*\] \[*recurrence_limit*\] \[*refund_for*\] \[*single_use*\]
**offer** *amount* *description* \[*vendor*\] \[*label*\] \[*quantity_min*\] \[*quantity_max*\] \[*absolute_expiry*\] \[*recurrence*\] \[*recurrence_base*\] \[*recurrence_paywindow*\] \[*recurrence_limit*\] \[*single_use*\]
DESCRIPTION
-----------
The **offer** RPC command creates an offer, which is a precursor to
one or more invoices. It automatically enables the accepting of
corresponding invoice_request or invoice messages (depending on
*send_invoice*).
creating one or more invoices. It automatically enables the processing of
an incoming invoice_request, and issuing of invoices.
The *amount* parameter can be the string "any", which creates an offer
that can be paid with any amount (e.g. a donation). Otherwise it can
@ -34,12 +33,6 @@ cannot use *\\u* JSON escape codes.
The *vendor* is another (optional) field exposed in the offer, and
reflects who is issuing this offer (i.e. you) if appropriate.
The *send_invoice* boolean (default false unless *single_use*) creates
an offer to send money: the user of the offer will send an invoice,
rather than an invoice_request. This is encoded in the offer. Note
that *recurrence* and ISO 4217 currencies are not currently
well-supported for this case!
The *label* field is an internal-use name for the offer, which can
be any UTF-8 string.
@ -87,13 +80,9 @@ period which exists. eg. "12" means there are 13 periods, from 0 to
This implies *send_invoice* and *single_use*. This is encoded in the
offer.
*single_use* (default false, unless *refund_for*) indicates that the
invoice associated with the offer is only valid once; for a
*send_invoice* offer many invoices can be accepted until one is
successfully paid (and we will only attempt to pay one at any time).
For a non-*single-use* offer, we will issue any number of invoices as
requested, until one is paid, at which time we will expire all the
other invoices for this offer and issue no more.
*single_use* (default false) indicates that the offer is only valid
once; we may issue multiple invoices, but as soon as one is paid all other
invoices will be expired (i.e. only one person can pay this offer).
RETURN VALUE
------------
@ -102,7 +91,7 @@ On success, an object as follows is returned:
* *offer_id*: the hash of the offer.
* *active*: true
* *single_use*: true if *single_use* was specified or implied.
* *single_use*: true if *single_use* was specified.
* *bolt12*: the bolt12 offer, starting with "lno1"
Optionally:
@ -125,7 +114,7 @@ Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible.
SEE ALSO
--------
lightning-listoffers(7), lightning-deloffer(7).
lightning-offerout(7), lightning-listoffers(7), lightning-deloffer(7).
RESOURCES
---------

113
doc/lightning-offerout.7

@ -0,0 +1,113 @@
.TH "LIGHTNING-OFFEROUT" "7" "" "" "lightning-offerout"
.SH NAME
lightning-offerout - Command for offering payments
.SH SYNOPSIS
\fIEXPERIMENTAL_FEATURES only\fR
\fBofferout\fR \fIamount\fR \fIdescription\fR [\fIvendor\fR] [\fIlabel\fR] [\fIabsolute_expiry\fR] [\fIrefund_for\fR]
.SH DESCRIPTION
The \fBofferout\fR RPC command creates an offer, which is a request to
send an invoice for us to pay (technically, this is referred to as a
\fBsend_invoice\fR offer to distinguish a normal \fBlightningd-offer\fR(7)
offer)\. It automatically enables the accepting and payment of
corresponding invoice message (we will only pay once, however!)\.
The \fIamount\fR parameter can be the string "any", which creates an offer
that can be paid with any amount (e\.g\. a donation)\. Otherwise it can
be a positive value in millisatoshi precision; it can be a whole
number, or a whole number ending in \fImsat\fR or \fIsat\fR, or a number with
three decimal places ending in \fIsat\fR, or a number with 1 to 11 decimal
places ending in \fIbtc\fR\.
The \fIdescription\fR is a short description of purpose of the offer,
e\.g\. \fIwithdrawl from ATM\fR\. This value is encoded into the resulting offer and is
viewable by anyone you expose this offer to\. It must be UTF-8, and
cannot use \fI\u\fR JSON escape codes\.
The \fIvendor\fR is another (optional) field exposed in the offer, and
reflects who is issuing this offer (i\.e\. you) if appropriate\.
The \fIlabel\fR field is an internal-use name for the offer, which can
be any UTF-8 string\.
The \fIabsolute_expiry\fR is optionally the time the offer is valid until,
in seconds since the first day of 1970 UTC\. If not set, the offer
remains valid (though it can be deactivated by the issuer of course)\.
This is encoded in the offer\.
\fIrefund_for\fR is a previous (paid) invoice of ours\. The
payment_preimage of this is encoded in the offer, and redemption
requires that the invoice we receive contains a valid signature using
that previous \fBpayer_key\fR\.
.SH RETURN VALUE
On success, an object as follows is returned:
.RS
.IP \[bu]
\fIoffer_id\fR: the hash of the offer\.
.IP \[bu]
\fIactive\fR: true
.IP \[bu]
\fIsingle_use\fR: true
.IP \[bu]
\fIbolt12\fR: the bolt12 offer, starting with "lno1"
.RE
Optionally:
.RS
.IP \[bu]
\fIlabel\fR: the user-specified label\.
.RE
On failure, an error is returned and no offer is created\. If the
lightning process fails before responding, the caller should use
\fBlightning-listoffers\fR(7) to query whether this offer was created or
not\.
The following error codes may occur:
.RS
.IP \[bu]
-1: Catchall nonspecific error\.
.IP \[bu]
1000: Offer with this offer_id already exists\.
.RE
.SH NOTES
The specification allows quantity, recurrence and alternate currencies on
offers which contain \fBsend_invoice\fR, but these are not implemented here\.
We could also allow multi-use offers, but usually you're only offering to
send money once\.
.SH AUTHOR
Rusty Russell \fI<rusty@rustcorp.com.au\fR> is mainly responsible\.
.SH SEE ALSO
\fBlightning-offer\fR(7), \fBlightning-listoffers\fR(7), \fBlightning-deloffer\fR(7)\.
.SH RESOURCES
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR
\" SHA256STAMP:092f0d776162906eb1045b31caccc7e5eb9fdfa7ba233570f867310ea441ebe5

93
doc/lightning-offerout.7.md

@ -0,0 +1,93 @@
lightning-offerout -- Command for offering payments
=================================================
SYNOPSIS
--------
*EXPERIMENTAL_FEATURES only*
**offerout** *amount* *description* \[*vendor*\] \[*label*\] \[*absolute_expiry*\] \[*refund_for*\]
DESCRIPTION
-----------
The **offerout** RPC command creates an offer, which is a request to
send an invoice for us to pay (technically, this is referred to as a
`send_invoice` offer to distinguish a normal lightningd-offer(7)
offer). It automatically enables the accepting and payment of
corresponding invoice message (we will only pay once, however!).
The *amount* parameter can be the string "any", which creates an offer
that can be paid with any amount (e.g. a donation). Otherwise it can
be a positive value in millisatoshi precision; it can be a whole
number, or a whole number ending in *msat* or *sat*, or a number with
three decimal places ending in *sat*, or a number with 1 to 11 decimal
places ending in *btc*.
The *description* is a short description of purpose of the offer,
e.g. *withdrawl from ATM*. This value is encoded into the resulting offer and is
viewable by anyone you expose this offer to. It must be UTF-8, and
cannot use *\\u* JSON escape codes.
The *vendor* is another (optional) field exposed in the offer, and
reflects who is issuing this offer (i.e. you) if appropriate.
The *label* field is an internal-use name for the offer, which can
be any UTF-8 string.
The *absolute_expiry* is optionally the time the offer is valid until,
in seconds since the first day of 1970 UTC. If not set, the offer
remains valid (though it can be deactivated by the issuer of course).
This is encoded in the offer.
*refund_for* is a previous (paid) invoice of ours. The
payment_preimage of this is encoded in the offer, and redemption
requires that the invoice we receive contains a valid signature using
that previous `payer_key`.
RETURN VALUE
------------
On success, an object as follows is returned:
* *offer_id*: the hash of the offer.
* *active*: true
* *single_use*: true
* *bolt12*: the bolt12 offer, starting with "lno1"
Optionally:
* *label*: the user-specified label.
On failure, an error is returned and no offer is created. If the
lightning process fails before responding, the caller should use
lightning-listoffers(7) to query whether this offer was created or
not.
The following error codes may occur:
- -1: Catchall nonspecific error.
- 1000: Offer with this offer_id already exists.
NOTES
-----
The specification allows quantity, recurrence and alternate currencies on
offers which contain `send_invoice`, but these are not implemented here.
We could also allow multi-use offers, but usually you're only offering to
send money once.
AUTHOR
------
Rusty Russell <<rusty@rustcorp.com.au>> is mainly responsible.
SEE ALSO
--------
lightning-offer(7), lightning-listoffers(7), lightning-deloffer(7).
RESOURCES
---------
Main web site: <https://github.com/ElementsProject/lightning>

11
plugins/offers.c

@ -133,10 +133,17 @@ static const struct plugin_command commands[] = {
{
"offer",
"payment",
"Create an offer",
"Create an offer for invoices of {amount} with {description}, optional {vendor}, {quantity_min}, {quantity_max}, {absolute_expiry}, {recurrence}, {recurrence_base}, {recurrence_paywindow}, {recurrence_limit} and {single_use}",
"Create an offer to accept money",
"Create an offer for invoices of {amount} with {description}, optional {vendor}, internal {label}, {quantity_min}, {quantity_max}, {absolute_expiry}, {recurrence}, {recurrence_base}, {recurrence_paywindow}, {recurrence_limit} and {single_use}",
json_offer
},
{
"offerout",
"payment",
"Create an offer to send money",
"Create an offer to pay invoices of {amount} with {description}, optional {vendor}, internal {label}, {absolute_expiry} and {refund_for}",
json_offerout
},
};
int main(int argc, char *argv[])

124
plugins/offers_offer.c

@ -7,25 +7,49 @@
#include <plugins/offers_offer.h>
#include <wire/onion_wire.h>
static bool msat_or_any(const char *buffer,
const jsmntok_t *tok,
struct tlv_offer *offer)
{
struct amount_msat msat;
if (json_tok_streq(buffer, tok, "any"))
return true;
if (!parse_amount_msat(&msat,
buffer + tok->start, tok->end - tok->start))
return false;
offer->amount = tal_dup(offer, u64,
&msat.millisatoshis); /* Raw: other currencies */
return true;
}
static struct command_result *param_msat_or_any(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
struct tlv_offer *offer)
{
if (msat_or_any(buffer, tok, offer))
return NULL;
return command_fail_badparam(cmd, name, buffer, tok,
"should be 'any' or msatoshis");
}
static struct command_result *param_amount(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
struct tlv_offer *offer)
{
struct amount_msat msat;
const struct iso4217_name_and_divisor *isocode;
jsmntok_t number, whole, frac;
u64 cents;
if (json_tok_streq(buffer, tok, "any"))
if (msat_or_any(buffer, tok, offer))
return NULL;
offer->amount = tal(offer, u64);
if (parse_amount_msat(&msat, buffer + tok->start, tok->end - tok->start)) {
*offer->amount = msat.millisatoshis; /* Raw: other currencies */
return NULL;
}
/* BOLT-offers #12:
*
@ -232,16 +256,15 @@ struct command_result *json_offer(struct command *cmd,
const char *desc, *vendor, *label;
struct tlv_offer *offer;
struct out_req *req;
bool *single_use, *send_invoice;
bool *single_use;
offer = tlv_offer_new(cmd);
if (!param(cmd, buffer, params,
p_req("amount", param_amount, offer),
p_req("description", param_escaped_string, &desc),
p_opt("send_invoice", param_bool, &send_invoice),
p_opt("label", param_escaped_string, &label),
p_opt("vendor", param_escaped_string, &vendor),
p_opt("label", param_escaped_string, &label),
p_opt("quantity_min", param_u64, &offer->quantity_min),
p_opt("quantity_max", param_u64, &offer->quantity_max),
p_opt("absolute_expiry", param_u64, &offer->absolute_expiry),
@ -255,8 +278,7 @@ struct command_result *json_offer(struct command *cmd,
p_opt("recurrence_limit",
param_number,
&offer->recurrence_limit),
p_opt("refund_for", param_invoice_payment_hash, &offer->refund_for),
p_opt("single_use", param_bool, &single_use),
p_opt_def("single_use", param_bool, &single_use, false),
/* FIXME: hints support! */
NULL))
return command_param_failed();
@ -273,32 +295,6 @@ struct command_result *json_offer(struct command *cmd,
offer->chains[0] = chainparams->genesis_blockhash;
}
/* If refund_for, send_invoice is true. */
if (offer->refund_for) {
if (!send_invoice) {
send_invoice = tal(cmd, bool);
*send_invoice = true;
}
if (!*send_invoice)
return command_fail_badparam(cmd, "refund_for",
buffer, params,
"needs send_invoice=true");
} else {
if (!send_invoice) {
send_invoice = tal(cmd, bool);
*send_invoice = false;
}
}
if (*send_invoice)
offer->send_invoice = tal(offer, struct tlv_offer_send_invoice);
/* single_use defaults to 'true' for send_invoices, false otherwise */
if (!single_use) {
single_use = tal(cmd, bool);
*single_use = offer->send_invoice ? true : false;
}
if (!offer->recurrence) {
if (offer->recurrence_limit)
return command_fail_badparam(cmd, "recurrence_limit",
@ -334,3 +330,57 @@ struct command_result *json_offer(struct command *cmd,
return send_outreq(cmd->plugin, req);
}
struct command_result *json_offerout(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
{
const char *desc, *vendor, *label;
struct tlv_offer *offer;
struct out_req *req;
offer = tlv_offer_new(cmd);
if (!param(cmd, buffer, params,
p_req("amount", param_msat_or_any, offer),
p_req("description", param_escaped_string, &desc),
p_opt("vendor", param_escaped_string, &vendor),
p_opt("label", param_escaped_string, &label),
p_opt("absolute_expiry", param_u64, &offer->absolute_expiry),
p_opt("refund_for", param_invoice_payment_hash, &offer->refund_for),
/* FIXME: hints support! */
NULL))
return command_param_failed();
offer->send_invoice = tal(offer, struct tlv_offer_send_invoice);
/* BOLT-offers #12:
*
* - if the chain for the invoice is not solely bitcoin:
* - MUST specify `chains` the offer is valid for.
* - otherwise:
* - the bitcoin chain is implied as the first and only entry.
*/
if (!streq(chainparams->network_name, "bitcoin")) {
offer->chains = tal_arr(offer, struct bitcoin_blkid, 1);
offer->chains[0] = chainparams->genesis_blockhash;
}
offer->description = tal_dup_arr(offer, char, desc, strlen(desc), 0);
if (vendor)
offer->vendor = tal_dup_arr(offer, char,
vendor, strlen(vendor), 0);
offer->node_id = tal_dup(offer, struct pubkey32, &id);
/* We simply pass this through. */
req = jsonrpc_request_start(cmd->plugin, cmd, "createoffer",
forward_result, forward_error,
offer);
json_add_string(req->js, "bolt12", offer_encode(tmpctx, offer));
if (label)
json_add_string(req->js, "label", label);
json_add_bool(req->js, "single_use", true);
return send_outreq(cmd->plugin, req);
}

4
plugins/offers_offer.h

@ -8,4 +8,8 @@ extern struct pubkey32 id;
struct command_result *json_offer(struct command *cmd,
const char *buffer,
const jsmntok_t *params);
struct command_result *json_offerout(struct command *cmd,
const char *buffer,
const jsmntok_t *params);
#endif /* LIGHTNING_PLUGINS_OFFERS_OFFER_H */

11
tests/test_pay.py

@ -3991,9 +3991,8 @@ def test_sendinvoice(node_factory, bitcoind):
l1, l2 = node_factory.line_graph(2, wait_for_announce=True)
# Simple offer to send money (balances channel a little)
offer = l1.rpc.call('offer', {'amount': '100000sat',
'description': 'simple test',
'send_invoice': True})['bolt12']
offer = l1.rpc.call('offerout', {'amount': '100000sat',
'description': 'simple test'})['bolt12']
print(offer)
# Fetchinvoice will refuse, since you're supposed to send an invoice.
@ -4019,9 +4018,9 @@ def test_sendinvoice(node_factory, bitcoind):
inv = l1.rpc.call('fetchinvoice', {'offer': offer})
l1.rpc.pay(inv['invoice'])
refund = l2.rpc.call('offer', {'amount': '100msat',
'description': 'refund test',
'refund_for': inv['invoice']})['bolt12']
refund = l2.rpc.call('offerout', {'amount': '100msat',
'description': 'refund test',
'refund_for': inv['invoice']})['bolt12']
l1.rpc.call('sendinvoice', {'offer': refund,
'label': 'test sendinvoice refund'})

Loading…
Cancel
Save