diff --git a/doc/Makefile b/doc/Makefile index df633dd6e..bf11cee1e 100644 --- a/doc/Makefile +++ b/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 \ diff --git a/doc/index.rst b/doc/index.rst index 42ce06c69..98d19a5e5 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -73,6 +73,7 @@ c-lightning Documentation lightning-newaddr lightning-notifications lightning-offer + lightning-offerout lightning-openchannel_init lightning-openchannel_signed lightning-openchannel_update diff --git a/doc/lightning-offer.7 b/doc/lightning-offer.7 index 7396e2230..7b12c80e5 100644 --- a/doc/lightning-offer.7 +++ b/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 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 diff --git a/doc/lightning-offer.7.md b/doc/lightning-offer.7.md index 56c57d6fe..12b5d1e9a 100644 --- a/doc/lightning-offer.7.md +++ b/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 <> is mainly responsible. SEE ALSO -------- -lightning-listoffers(7), lightning-deloffer(7). +lightning-offerout(7), lightning-listoffers(7), lightning-deloffer(7). RESOURCES --------- diff --git a/doc/lightning-offerout.7 b/doc/lightning-offerout.7 new file mode 100644 index 000000000..36cc4a352 --- /dev/null +++ b/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 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 diff --git a/doc/lightning-offerout.7.md b/doc/lightning-offerout.7.md new file mode 100644 index 000000000..257fca7ef --- /dev/null +++ b/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 <> is mainly responsible. + +SEE ALSO +-------- + +lightning-offer(7), lightning-listoffers(7), lightning-deloffer(7). + +RESOURCES +--------- + +Main web site: + diff --git a/plugins/offers.c b/plugins/offers.c index a51ad7988..9bc44bca2 100644 --- a/plugins/offers.c +++ b/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[]) diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index 5ab0fef38..7c227b95b 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -7,25 +7,49 @@ #include #include +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); +} + diff --git a/plugins/offers_offer.h b/plugins/offers_offer.h index ccc3a34ab..1234dc0e9 100644 --- a/plugins/offers_offer.h +++ b/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 */ diff --git a/tests/test_pay.py b/tests/test_pay.py index 1e734dd87..51c30601d 100644 --- a/tests/test_pay.py +++ b/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'})