offers: remove 'send-invoice' offers support.

This has radically changed in the spec, so remove it now, and we'll
reintroduce / rewrite it.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
Rusty Russell 2022-11-09 13:02:00 +10:30 committed by Christian Decker
parent 3afa5077fe
commit 846a520bc2
7 changed files with 3 additions and 998 deletions

View file

@ -31,7 +31,7 @@ PLUGIN_PAY_LIB_SRC := plugins/libplugin-pay.c
PLUGIN_PAY_LIB_HEADER := plugins/libplugin-pay.h
PLUGIN_PAY_LIB_OBJS := $(PLUGIN_PAY_LIB_SRC:.c=.o)
PLUGIN_OFFERS_SRC := plugins/offers.c plugins/offers_offer.c plugins/offers_invreq_hook.c plugins/offers_inv_hook.c
PLUGIN_OFFERS_SRC := plugins/offers.c plugins/offers_offer.c plugins/offers_invreq_hook.c
PLUGIN_OFFERS_OBJS := $(PLUGIN_OFFERS_SRC:.c=.o)
PLUGIN_OFFERS_HEADER := $(PLUGIN_OFFERS_SRC:.c=.h)

View file

@ -751,32 +751,6 @@ static struct command_result *send_message(struct command *cmd,
return make_reply_path(cmd, sending);
}
/* We've received neither a reply nor a payment; return failure. */
static void timeout_sent_inv(struct sent *sent)
{
struct json_out *details = json_out_new(sent);
json_out_start(details, NULL, '{');
json_out_addstr(details, "invstring", invoice_encode(tmpctx, sent->inv));
json_out_end(details, '}');
/* This will free sent! */
discard_result(command_done_err(sent->cmd, OFFER_TIMEOUT,
"Failed: timeout waiting for response",
details));
}
static struct command_result *prepare_inv_timeout(struct command *cmd,
const char *buf UNUSED,
const jsmntok_t *result UNUSED,
struct sent *sent)
{
tal_steal(cmd, plugin_timer(cmd->plugin,
time_from_sec(sent->wait_timeout),
timeout_sent_inv, sent));
return sendonionmsg_done(cmd, buf, result, sent);
}
/* We've connected (if we tried), so send the invreq. */
static struct command_result *
sendinvreq_after_connect(struct command *cmd,
@ -1276,354 +1250,6 @@ static struct command_result *invoice_payment(struct command *cmd,
return command_hook_success(cmd);
}
/* We've connected (if we tried), so send the invoice. */
static struct command_result *
sendinvoice_after_connect(struct command *cmd,
const char *buf UNUSED,
const jsmntok_t *result UNUSED,
struct sent *sent)
{
struct tlv_onionmsg_tlv *payload = tlv_onionmsg_tlv_new(sent);
payload->invoice = tal_arr(payload, u8, 0);
towire_tlv_invoice(&payload->invoice, sent->inv);
return send_message(cmd, sent, payload, prepare_inv_timeout);
}
static struct command_result *createinvoice_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct sent *sent)
{
const jsmntok_t *invtok = json_get_member(buf, result, "bolt12");
char *fail;
/* Replace invoice with signed one */
tal_free(sent->inv);
sent->inv = invoice_decode(sent,
buf + invtok->start,
invtok->end - invtok->start,
plugin_feature_set(cmd->plugin),
chainparams,
&fail);
if (!sent->inv) {
plugin_log(cmd->plugin, LOG_BROKEN,
"Bad createinvoice %.*s: %s",
json_tok_full_len(invtok),
json_tok_full(buf, invtok),
fail);
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Bad createinvoice response %s", fail);
}
sent->path = path_to_node(sent, cmd->plugin,
sent->offer->node_id);
if (!sent->path)
return connect_direct(cmd, sent->offer->node_id,
sendinvoice_after_connect, sent);
return sendinvoice_after_connect(cmd, NULL, NULL, sent);
}
static struct command_result *sign_invoice(struct command *cmd,
struct sent *sent)
{
struct out_req *req;
/* Get invoice signature and put in db so we can receive payment */
req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoice",
&createinvoice_done,
&forward_error,
sent);
json_add_string(req->js, "invstring", invoice_encode(tmpctx, sent->inv));
json_add_preimage(req->js, "preimage", &sent->inv_preimage);
json_add_escaped_string(req->js, "label", sent->inv_label);
return send_outreq(cmd->plugin, req);
}
static bool json_to_bip340sig(const char *buffer, const jsmntok_t *tok,
struct bip340sig *sig)
{
return hex_decode(buffer + tok->start, tok->end - tok->start,
sig->u8, sizeof(sig->u8));
}
static struct command_result *payersign_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct sent *sent)
{
const jsmntok_t *sig;
sent->inv->refund_signature = tal(sent->inv, struct bip340sig);
sig = json_get_member(buf, result, "signature");
json_to_bip340sig(buf, sig, sent->inv->refund_signature);
return sign_invoice(cmd, sent);
}
/* They're offering a refund, so we need to sign with same key as used
* in initial payment. */
static struct command_result *listsendpays_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct sent *sent)
{
const jsmntok_t *t, *arr = json_get_member(buf, result, "payments");
size_t i;
const u8 *public_tweak = NULL, *p;
u8 *msg;
size_t len;
struct sha256 merkle;
struct out_req *req;
/* Linearize populates ->fields */
msg = tal_arr(tmpctx, u8, 0);
towire_tlv_invoice(&msg, sent->inv);
p = msg;
len = tal_bytelen(msg);
sent->inv = fromwire_tlv_invoice(cmd, &p, &len);
if (!sent->inv)
plugin_err(cmd->plugin,
"Could not remarshall %s", tal_hex(tmpctx, msg));
merkle_tlv(sent->inv->fields, &merkle);
json_for_each_arr(i, t, arr) {
const jsmntok_t *b12tok;
struct tlv_invoice *inv;
char *fail;
b12tok = json_get_member(buf, t, "bolt12");
if (!b12tok) {
/* This could happen if they try to refund a bolt11 */
plugin_log(cmd->plugin, LOG_UNUSUAL,
"Not bolt12 string in %.*s?",
json_tok_full_len(t),
json_tok_full(buf, t));
continue;
}
inv = invoice_decode(tmpctx, buf + b12tok->start,
b12tok->end - b12tok->start,
plugin_feature_set(cmd->plugin),
chainparams,
&fail);
if (!inv) {
plugin_log(cmd->plugin, LOG_BROKEN,
"Bad bolt12 string in %.*s?",
json_tok_full_len(t),
json_tok_full(buf, t));
continue;
}
public_tweak = inv->payer_info;
break;
}
if (!public_tweak)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Cannot find invoice %s for refund",
type_to_string(tmpctx, struct sha256,
sent->offer->refund_for));
/* BOLT-offers #12:
* - MUST set `refund_signature` to the signature of the
* `refunded_payment_hash` using prefix `refund_signature` and the
* `payer_key` from the to-be-refunded invoice.
*/
req = jsonrpc_request_start(cmd->plugin, cmd, "payersign",
&payersign_done,
&forward_error,
sent);
json_add_string(req->js, "messagename", "invoice");
json_add_string(req->js, "fieldname", "refund_signature");
json_add_sha256(req->js, "merkle", &merkle);
json_add_hex_talarr(req->js, "tweak", public_tweak);
return send_outreq(cmd->plugin, req);
}
static struct command_result *json_sendinvoice(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
{
struct amount_msat *msat;
struct out_req *req;
u32 *timeout;
struct sent *sent = tal(cmd, struct sent);
sent->inv = tlv_invoice_new(cmd);
sent->invreq = NULL;
sent->cmd = cmd;
/* FIXME: Support recurring send_invoice offers? */
if (!param(cmd, buffer, params,
p_req("offer", param_offer, &sent->offer),
p_req("label", param_label, &sent->inv_label),
p_opt("amount_msat|msatoshi", param_msat, &msat),
p_opt_def("timeout", param_number, &timeout, 90),
p_opt("quantity", param_u64, &sent->inv->quantity),
NULL))
return command_param_failed();
/* This is how long we'll wait for a reply for. */
sent->wait_timeout = *timeout;
/* Check they are really trying to send us money. */
if (!sent->offer->send_invoice)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Offer wants an invoice_request, not invoice");
/* If they don't tell us how much, base it on offer. */
if (!msat) {
if (sent->offer->currency)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Offer in different currency: need amount");
if (!sent->offer->amount)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Offer did not specify: need amount");
sent->inv->amount = tal_dup(sent->inv, u64, sent->offer->amount);
if (sent->inv->quantity)
*sent->inv->amount *= *sent->inv->quantity;
} else
sent->inv->amount = tal_dup(sent->inv, u64,
&msat->millisatoshis); /* Raw: tlv */
/* FIXME: Support blinded paths, in which case use fake nodeid */
/* BOLT-offers #12:
* - otherwise (responding to a `send_invoice` offer):
* - MUST set `node_id` to the id of the node to send payment to.
* - MUST set `description` the same as the offer.
*/
sent->inv->node_id = tal(sent->inv, struct pubkey);
sent->inv->node_id->pubkey = local_id.pubkey;
sent->inv->description
= tal_dup_talarr(sent->inv, char, sent->offer->description);
/* BOLT-offers #12:
* - MUST set (or not set) `send_invoice` the same as the offer.
*/
sent->inv->send_invoice = tal(sent->inv, struct tlv_invoice_send_invoice);
/* BOLT-offers #12:
* - MUST set `offer_id` to the id of the offer.
*/
sent->inv->offer_id = tal(sent->inv, struct sha256);
merkle_tlv(sent->offer->fields, sent->inv->offer_id);
/* BOLT-offers #12:
* - SHOULD not respond to an offer if the current time is after
* `absolute_expiry`.
*/
if (sent->offer->absolute_expiry
&& time_now().ts.tv_sec > *sent->offer->absolute_expiry)
return command_fail(cmd, OFFER_EXPIRED, "Offer expired");
/* BOLT-offers #12:
* - otherwise (responding to a `send_invoice` offer):
*...
* - if the offer had a `quantity_min` or `quantity_max` field:
* - MUST set `quantity`
* - MUST set it within that (inclusive) range.
* - otherwise:
* - MUST NOT set `quantity`
*/
if (sent->offer->quantity_min || sent->offer->quantity_max) {
if (!sent->inv->quantity)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity parameter required");
if (sent->offer->quantity_min
&& *sent->inv->quantity < *sent->offer->quantity_min)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity must be >= %"PRIu64,
*sent->offer->quantity_min);
if (sent->offer->quantity_max
&& *sent->inv->quantity > *sent->offer->quantity_max)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity must be <= %"PRIu64,
*sent->offer->quantity_max);
} else {
if (sent->inv->quantity)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"quantity parameter unnecessary");
}
/* BOLT-offers #12:
* - MUST set `created_at` to the number of seconds since Midnight 1
* January 1970, UTC when the offer was created.
*/
sent->inv->created_at = tal(sent->inv, u64);
*sent->inv->created_at = time_now().ts.tv_sec;
/* BOLT-offers #12:
* - if the expiry for accepting payment is not 7200 seconds after
* `created_at`:
* - MUST set `relative_expiry` `seconds_from_creation` to the number
* of seconds after `created_at` that payment of this invoice should
* not be attempted.
*/
if (sent->wait_timeout != 7200) {
sent->inv->relative_expiry = tal(sent->inv, u32);
*sent->inv->relative_expiry = sent->wait_timeout;
}
/* BOLT-offers #12:
* - MUST set `payer_key` to the `node_id` of the offer.
*/
sent->inv->payer_key = sent->offer->node_id;
/* FIXME: recurrence? */
if (sent->offer->recurrence)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"FIXME: handle recurring send_invoice offer!");
/* 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")) {
sent->inv->chain = tal_dup(sent->inv, struct bitcoin_blkid,
&chainparams->genesis_blockhash);
}
sent->inv->features
= plugin_feature_set(cmd->plugin)->bits[BOLT11_FEATURE];
randombytes_buf(&sent->inv_preimage, sizeof(sent->inv_preimage));
sent->inv->payment_hash = tal(sent->inv, struct sha256);
sha256(sent->inv->payment_hash,
&sent->inv_preimage, sizeof(sent->inv_preimage));
/* BOLT-offers #12:
* - MUST set (or not set) `refund_for` exactly as the offer did.
* - if it sets `refund_for`:
* - MUST set `refund_signature` to the signature of the
* `refunded_payment_hash` using prefix `refund_signature` and
* the `payer_key` from the to-be-refunded invoice.
* - otherwise:
* - MUST NOT set `refund_signature`
*/
if (sent->offer->refund_for) {
sent->inv->refund_for = sent->offer->refund_for;
/* Find original payment invoice */
req = jsonrpc_request_start(cmd->plugin, cmd, "listsendpays",
&listsendpays_done,
&forward_error,
sent);
json_add_sha256(req->js, "payment_hash",
sent->offer->refund_for);
return send_outreq(cmd->plugin, req);
}
return sign_invoice(cmd, sent);
}
#if DEVELOPER
static struct command_result *param_invreq(struct command *cmd,
const char *name,
@ -1682,13 +1308,6 @@ static const struct plugin_command commands[] = {
NULL,
json_fetchinvoice,
},
{
"sendinvoice",
"payment",
"Request remote node for to pay this send_invoice {offer}, with {amount}, {quanitity}, {recurrence_counter}, {recurrence_start} and {recurrence_label} iff required.",
NULL,
json_sendinvoice,
},
#if DEVELOPER
{
"dev-rawrequest",

View file

@ -14,7 +14,6 @@
#include <common/json_param.h>
#include <common/json_stream.h>
#include <plugins/offers.h>
#include <plugins/offers_inv_hook.h>
#include <plugins/offers_invreq_hook.h>
#include <plugins/offers_offer.h>
@ -90,7 +89,7 @@ static struct command_result *onion_message_modern_call(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
const jsmntok_t *om, *replytok, *invreqtok, *invtok;
const jsmntok_t *om, *replytok, *invreqtok;
struct blinded_path *reply_path = NULL;
if (!offers_enabled)
@ -118,13 +117,6 @@ static struct command_result *onion_message_modern_call(struct command *cmd,
"invoice_request without reply_path");
}
invtok = json_get_member(buf, om, "invoice");
if (invtok) {
const u8 *invbin = json_tok_bin_from_hex(tmpctx, buf, invtok);
if (invbin)
return handle_invoice(cmd, invbin, reply_path);
}
return command_hook_success(cmd);
}
@ -969,13 +961,6 @@ static const struct plugin_command commands[] = {
"Create an offer for invoices of {amount} with {description}, optional {issuer}, 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 {issuer}, internal {label}, {absolute_expiry} and {refund_for}",
json_offerout
},
{
"decode",
"utility",

View file

@ -1,401 +0,0 @@
#include "config.h"
#include <ccan/mem/mem.h>
#include <ccan/tal/str/str.h>
#include <common/bolt12_merkle.h>
#include <common/json_stream.h>
#include <common/type_to_string.h>
#include <plugins/offers.h>
#include <plugins/offers_inv_hook.h>
#include <secp256k1_schnorrsig.h>
/* We need to keep the reply path around so we can reply if error */
struct inv {
struct tlv_invoice *inv;
/* May be NULL */
struct blinded_path *reply_path;
/* The offer, once we've looked it up. */
struct tlv_offer *offer;
};
static struct command_result *WARN_UNUSED_RESULT
fail_inv_level(struct command *cmd,
const struct inv *inv,
enum log_level l,
const char *fmt, va_list ap)
{
char *full_fmt, *msg;
struct tlv_onionmsg_tlv *payload;
struct tlv_invoice_error *err;
full_fmt = tal_fmt(tmpctx, "Failed invoice");
if (inv->inv) {
tal_append_fmt(&full_fmt, " %s",
invoice_encode(tmpctx, inv->inv));
if (inv->inv->offer_id)
tal_append_fmt(&full_fmt, " for offer %s",
type_to_string(tmpctx, struct sha256,
inv->inv->offer_id));
}
tal_append_fmt(&full_fmt, ": %s", fmt);
msg = tal_vfmt(tmpctx, full_fmt, ap);
plugin_log(cmd->plugin, l, "%s", msg);
/* Only reply if they gave us a path */
if (!inv->reply_path)
return command_hook_success(cmd);
/* Don't send back internal error details. */
if (l == LOG_BROKEN)
msg = "Internal error";
err = tlv_invoice_error_new(cmd);
/* Remove NUL terminator */
err->error = tal_dup_arr(err, char, msg, strlen(msg), 0);
/* FIXME: Add suggested_value / erroneous_field! */
payload = tlv_onionmsg_tlv_new(tmpctx);
payload->invoice_error = tal_arr(payload, u8, 0);
towire_tlv_invoice_error(&payload->invoice_error, err);
return send_onion_reply(cmd, inv->reply_path, payload);
}
static struct command_result *WARN_UNUSED_RESULT
fail_inv(struct command *cmd,
const struct inv *inv,
const char *fmt, ...)
{
va_list ap;
struct command_result *ret;
va_start(ap, fmt);
ret = fail_inv_level(cmd, inv, LOG_DBG, fmt, ap);
va_end(ap);
return ret;
}
static struct command_result *WARN_UNUSED_RESULT
fail_internalerr(struct command *cmd,
const struct inv *inv,
const char *fmt, ...)
{
va_list ap;
struct command_result *ret;
va_start(ap, fmt);
ret = fail_inv_level(cmd, inv, LOG_BROKEN, fmt, ap);
va_end(ap);
return ret;
}
#define inv_must_have(cmd_, i_, fld_) \
test_field(cmd_, i_, i_->inv->fld_ != NULL, #fld_, "missing")
#define inv_must_not_have(cmd_, i_, fld_) \
test_field(cmd_, i_, i_->inv->fld_ == NULL, #fld_, "unexpected")
#define inv_must_equal_offer(cmd_, i_, fld_) \
test_field_eq(cmd_, i_, i_->inv->fld_, i_->offer->fld_, #fld_)
static struct command_result *
test_field(struct command *cmd,
const struct inv *inv,
bool test, const char *fieldname, const char *what)
{
if (!test)
return fail_inv(cmd, inv, "%s %s", what, fieldname);
return NULL;
}
static struct command_result *
test_field_eq(struct command *cmd,
const struct inv *inv,
const tal_t *invfield,
const tal_t *offerfield,
const char *fieldname)
{
if (invfield && !offerfield)
return fail_inv(cmd, inv, "Unexpected %s", fieldname);
if (!invfield && offerfield)
return fail_inv(cmd, inv, "Expected %s", fieldname);
if (!memeq(invfield, tal_bytelen(invfield),
offerfield, tal_bytelen(offerfield)))
return fail_inv(cmd, inv, "Different %s", fieldname);
return NULL;
}
static struct command_result *pay_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct inv *inv)
{
struct amount_msat msat = amount_msat(*inv->inv->amount);
plugin_log(cmd->plugin, LOG_INFORM,
"Payed out %s for offer %s%s: %.*s",
type_to_string(tmpctx, struct amount_msat, &msat),
type_to_string(tmpctx, struct sha256, inv->inv->offer_id),
inv->offer->refund_for ? " (refund)": "",
json_tok_full_len(result),
json_tok_full(buf, result));
return command_hook_success(cmd);
}
static struct command_result *pay_error(struct command *cmd,
const char *buf,
const jsmntok_t *error,
struct inv *inv)
{
const jsmntok_t *msgtok = json_get_member(buf, error, "message");
return fail_inv(cmd, inv, "pay attempt failed: %.*s",
json_tok_full_len(msgtok),
json_tok_full(buf, msgtok));
}
static struct command_result *listoffers_done(struct command *cmd,
const char *buf,
const jsmntok_t *result,
struct inv *inv)
{
const jsmntok_t *arr = json_get_member(buf, result, "offers");
const jsmntok_t *offertok, *activetok, *b12tok;
bool active;
struct amount_msat amt;
char *fail;
struct out_req *req;
struct command_result *err;
/* BOLT-offers #12:
* - otherwise if `offer_id` is set:
* - MUST reject the invoice if the `offer_id` does not refer an
* unexpired offer with `send_invoice`
*/
if (arr->size == 0)
return fail_inv(cmd, inv, "Unknown offer");
plugin_log(cmd->plugin, LOG_INFORM,
"Attempting payment of offer %.*s",
json_tok_full_len(result),
json_tok_full(buf, result));
offertok = arr + 1;
activetok = json_get_member(buf, offertok, "active");
if (!activetok) {
return fail_internalerr(cmd, inv,
"Missing active: %.*s",
json_tok_full_len(offertok),
json_tok_full(buf, offertok));
}
json_to_bool(buf, activetok, &active);
if (!active)
return fail_inv(cmd, inv, "Offer no longer available");
b12tok = json_get_member(buf, offertok, "bolt12");
if (!b12tok) {
return fail_internalerr(cmd, inv,
"Missing bolt12: %.*s",
json_tok_full_len(offertok),
json_tok_full(buf, offertok));
}
inv->offer = offer_decode(inv,
buf + b12tok->start,
b12tok->end - b12tok->start,
plugin_feature_set(cmd->plugin),
chainparams, &fail);
if (!inv->offer) {
return fail_internalerr(cmd, inv,
"Invalid offer: %s (%.*s)",
fail,
json_tok_full_len(offertok),
json_tok_full(buf, offertok));
}
if (inv->offer->absolute_expiry
&& time_now().ts.tv_sec >= *inv->offer->absolute_expiry) {
/* FIXME: do deloffer to disable it */
return fail_inv(cmd, inv, "Offer expired");
}
if (!inv->offer->send_invoice) {
return fail_inv(cmd, inv, "Offer did not expect invoice");
}
/* BOLT-offers #12:
* - MUST reject the invoice unless the following fields are equal
* or unset exactly as they are in the `offer`:
* - `refund_for`
* - `description`
*/
err = inv_must_equal_offer(cmd, inv, refund_for);
if (err)
return err;
err = inv_must_equal_offer(cmd, inv, description);
if (err)
return err;
/* BOLT-offers #12:
* - if the offer had a `quantity_min` or `quantity_max` field:
* - MUST fail the request if there is no `quantity` field.
* - MUST fail the request if there is `quantity` is not within
* that (inclusive) range.
* - otherwise:
* - MUST fail the request if there is a `quantity` field.
*/
if (inv->offer->quantity_min || inv->offer->quantity_max) {
err = inv_must_have(cmd, inv, quantity);
if (err)
return err;
if (inv->offer->quantity_min &&
*inv->inv->quantity < *inv->offer->quantity_min) {
return fail_inv(cmd, inv,
"quantity %"PRIu64 " < %"PRIu64,
*inv->inv->quantity,
*inv->offer->quantity_min);
}
if (inv->offer->quantity_max &&
*inv->inv->quantity > *inv->offer->quantity_max) {
return fail_inv(cmd, inv,
"quantity %"PRIu64" > %"PRIu64,
*inv->inv->quantity,
*inv->offer->quantity_max);
}
} else {
err = inv_must_not_have(cmd, inv, quantity);
if (err)
return err;
}
/* BOLT-offers #12:
* - MUST reject the invoice if `msat` is not present.
*/
err = inv_must_have(cmd, inv, amount);
if (err)
return err;
/* FIXME: Handle alternate currency conversion here! */
if (inv->offer->currency)
return fail_inv(cmd, inv, "FIXME: support currency");
amt = amount_msat(*inv->inv->amount);
/* If you send an offer without an amount, you want to give away
* unlimited money. Err, ok? */
if (inv->offer->amount) {
struct amount_msat expected = amount_msat(*inv->offer->amount);
/* We could allow invoices for less, I suppose. */
if (!amount_msat_eq(expected, amt))
return fail_inv(cmd, inv, "Expected invoice for %s",
fmt_amount_msat(tmpctx, expected));
}
plugin_log(cmd->plugin, LOG_INFORM,
"Attempting payment of %s for offer %s%s",
type_to_string(tmpctx, struct amount_msat, &amt),
type_to_string(tmpctx, struct sha256, inv->inv->offer_id),
inv->offer->refund_for ? " (refund)": "");
req = jsonrpc_request_start(cmd->plugin, cmd, "pay",
pay_done, pay_error, inv);
json_add_string(req->js, "bolt11", invoice_encode(tmpctx, inv->inv));
json_add_sha256(req->js, "localofferid", inv->inv->offer_id);
return send_outreq(cmd->plugin, req);
}
static struct command_result *listoffers_error(struct command *cmd,
const char *buf,
const jsmntok_t *err,
struct inv *inv)
{
return fail_internalerr(cmd, inv,
"listoffers gave JSON error: %.*s",
json_tok_full_len(err),
json_tok_full(buf, err));
}
struct command_result *handle_invoice(struct command *cmd,
const u8 *invbin,
struct blinded_path *reply_path STEALS)
{
size_t len = tal_count(invbin);
struct inv *inv = tal(cmd, struct inv);
struct out_req *req;
struct command_result *err;
int bad_feature;
struct sha256 m, shash;
inv->reply_path = tal_steal(inv, reply_path);
inv->inv = fromwire_tlv_invoice(cmd, &invbin, &len);
if (!inv->inv) {
return fail_inv(cmd, inv,
"Invalid invoice %s",
tal_hex(tmpctx, invbin));
}
/* BOLT-offers #12:
*
* The reader of an invoice_request:
*...
* - MUST fail the request if `features` contains unknown even bits.
*/
bad_feature = features_unsupported(plugin_feature_set(cmd->plugin),
inv->inv->features,
BOLT11_FEATURE);
if (bad_feature != -1) {
return fail_inv(cmd, inv,
"Unsupported inv feature %i",
bad_feature);
}
/* BOLT-offers #12:
*
* The reader of an invoice_request:
*...
* - if `chain` is not present:
* - MUST fail the request if bitcoin is not a supported chain.
* - otherwise:
* - MUST fail the request if `chain` is not a supported chain.
*/
if (!bolt12_chain_matches(inv->inv->chain, chainparams)) {
return fail_inv(cmd, inv,
"Wrong chain %s",
tal_hex(tmpctx, inv->inv->chain));
}
/* BOLT-offers #12:
* - MUST reject the invoice if `signature` is not a valid signature
* using `node_id` as described in
* [Signature Calculation](#signature-calculation).
*/
err = inv_must_have(cmd, inv, node_id);
if (err)
return err;
err = inv_must_have(cmd, inv, signature);
if (err)
return err;
merkle_tlv(inv->inv->fields, &m);
sighash_from_merkle("invoice", "signature", &m, &shash);
if (!check_schnorr_sig(&shash, &inv->inv->node_id->pubkey,
inv->inv->signature)) {
return fail_inv(cmd, inv, "Bad signature");
}
/* We don't pay random invoices off the internet, sorry. */
err = inv_must_have(cmd, inv, offer_id);
if (err)
return err;
/* Now find the offer. */
req = jsonrpc_request_start(cmd->plugin, cmd, "listoffers",
listoffers_done, listoffers_error, inv);
json_add_sha256(req->js, "offer_id", inv->inv->offer_id);
return send_outreq(cmd->plugin, req);
}

View file

@ -1,10 +0,0 @@
#ifndef LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H
#define LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H
#include "config.h"
#include <plugins/libplugin.h>
/* We got an onionmessage with an invoice! reply_path could be NULL. */
struct command_result *handle_invoice(struct command *cmd,
const u8 *invbin,
struct blinded_path *reply_path STEALS);
#endif /* LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H */

View file

@ -26,18 +26,6 @@ static bool msat_or_any(const char *buffer,
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,
@ -225,32 +213,6 @@ static struct command_result *param_recurrence_paywindow(struct command *cmd,
return NULL;
}
static struct command_result *param_invoice_payment_hash(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
struct sha256 **hash)
{
struct tlv_invoice *inv;
char *fail;
inv = invoice_decode(tmpctx, buffer + tok->start, tok->end - tok->start,
plugin_feature_set(cmd->plugin), chainparams,
&fail);
if (!inv)
return command_fail_badparam(cmd, name, buffer, tok,
tal_fmt(cmd,
"Unparsable invoice: %s",
fail));
if (!inv->payment_hash)
return command_fail_badparam(cmd, name, buffer, tok,
"invoice missing payment_hash");
*hash = tal_steal(cmd, inv->payment_hash);
return NULL;
}
struct offer_info {
const struct tlv_offer *offer;
const char *label;
@ -424,61 +386,3 @@ struct command_result *json_offer(struct command *cmd,
return create_offer(cmd, offinfo);
}
struct command_result *json_offerout(struct command *cmd,
const char *buffer,
const jsmntok_t *params)
{
const char *desc, *issuer, *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("issuer", param_escaped_string, &issuer),
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();
if (!offers_enabled)
return command_fail(cmd, LIGHTNINGD,
"experimental-offers not enabled");
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 (issuer)
offer->issuer = tal_dup_arr(offer, char,
issuer, strlen(issuer), 0);
offer->node_id = tal_dup(offer, struct pubkey, &id);
req = jsonrpc_request_start(cmd->plugin, cmd, "createoffer",
check_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);
}

View file

@ -4379,9 +4379,6 @@ def test_offer_needs_option(node_factory):
l1 = node_factory.get_node()
with pytest.raises(RpcError, match='experimental-offers not enabled'):
l1.rpc.call('offer', {'amount': '1msat', 'description': 'test'})
with pytest.raises(RpcError, match='experimental-offers not enabled'):
l1.rpc.call('offerout', {'amount': '2msat',
'description': 'simple test'})
with pytest.raises(RpcError, match='Unknown command'):
l1.rpc.call('fetchinvoice', {'offer': 'aaaa'})
@ -4823,16 +4820,6 @@ def test_fetchinvoice_autoconnect(node_factory, bitcoind):
l3.rpc.call('fetchinvoice', {'offer': offer['bolt12']})
assert l3.rpc.listpeers(l2.info['id'])['peers'] != []
# Similarly for send-invoice offer.
l3.rpc.disconnect(l2.info['id'])
offer = l2.rpc.call('offerout', {'amount': '2msat',
'description': 'simple test'})
# Ofc l2 can't actually pay it!
with pytest.raises(RpcError, match='pay attempt failed: "Ran out of routes to try'):
l3.rpc.call('sendinvoice', {'offer': offer['bolt12'], 'label': 'payme!'})
assert l3.rpc.listpeers(l2.info['id'])['peers'] != []
# But if we create a channel l3->l1->l2 (and balance!), l2 can!
node_factory.join_nodes([l3, l1], wait_for_announce=True)
# Make sure l2 knows about it
@ -4843,7 +4830,7 @@ def test_fetchinvoice_autoconnect(node_factory, bitcoind):
wait_for(lambda: only_one(only_one(l2.rpc.listpeers(l1.info['id'])['peers'])['channels'])['spendable_msat'] != Millisatoshi(0))
l3.rpc.disconnect(l2.info['id'])
l3.rpc.call('sendinvoice', {'offer': offer['bolt12'], 'label': 'payme for real!'})
l3.rpc.call('fetchinvoice', {'offer': offer['bolt12']})
# It will have autoconnected, to send invoice (since l1 says it doesn't do onion messages!)
assert l3.rpc.listpeers(l2.info['id'])['peers'] != []
@ -4883,85 +4870,6 @@ def test_dev_rawrequest(node_factory):
assert 'invoice' in ret
def test_sendinvoice(node_factory, bitcoind):
l2opts = {'experimental-offers': None}
l1, l2 = node_factory.line_graph(2, wait_for_announce=True,
opts=[{'experimental-offers': None},
l2opts])
# Simple offer to send money (balances channel a little)
offer = l1.rpc.call('offerout', {'amount': '100000sat',
'description': 'simple test'})
# Fetchinvoice will refuse, since you're supposed to send an invoice.
with pytest.raises(RpcError, match='Offer wants an invoice, not invoice_request'):
l2.rpc.call('fetchinvoice', {'offer': offer['bolt12']})
# used will be false
assert only_one(l1.rpc.call('listoffers', [offer['offer_id']])['offers'])['used'] is False
# sendinvoice should work.
out = l2.rpc.call('sendinvoice', {'offer': offer['bolt12'],
'label': 'test sendinvoice 1'})
assert out['label'] == 'test sendinvoice 1'
assert out['description'] == 'simple test'
assert 'bolt12' in out
assert 'payment_hash' in out
assert out['status'] == 'paid'
assert 'payment_preimage' in out
assert 'expires_at' in out
assert out['amount_msat'] == Millisatoshi(100000000)
assert 'pay_index' in out
assert out['amount_received_msat'] == Millisatoshi(100000000)
# Note, if we're slow, this fails with "Offer no longer available",
# *but* if it hasn't heard about payment success yet, l2 will fail
# simply because payments are already pending.
with pytest.raises(RpcError, match='Offer no longer available|pay attempt failed'):
l2.rpc.call('sendinvoice', {'offer': offer['bolt12'],
'label': 'test sendinvoice 2'})
# Technically, l1 may not have gotten payment success, so we need to wait.
wait_for(lambda: only_one(l1.rpc.call('listoffers', [offer['offer_id']])['offers'])['used'] is True)
# Now try a refund.
offer = l2.rpc.call('offer', {'amount': '100msat',
'description': 'simple test'})
assert only_one(l2.rpc.call('listoffers', [offer['offer_id']])['offers'])['used'] is False
inv = l1.rpc.call('fetchinvoice', {'offer': offer['bolt12']})
l1.rpc.pay(inv['invoice'])
assert only_one(l2.rpc.call('listoffers', [offer['offer_id']])['offers'])['used'] is True
refund = l2.rpc.call('offerout', {'amount': '100msat',
'description': 'refund test',
'refund_for': inv['invoice']})
assert only_one(l2.rpc.call('listoffers', [refund['offer_id']])['offers'])['used'] is False
l1.rpc.call('sendinvoice', {'offer': refund['bolt12'],
'label': 'test sendinvoice refund'})
wait_for(lambda: only_one(l2.rpc.call('listoffers', [refund['offer_id']])['offers'])['used'] is True)
# Offer with issuer: we must not copy issuer into our invoice!
offer = l1.rpc.call('offerout', {'amount': '10000sat',
'description': 'simple test',
'issuer': "clightning test suite"})
out = l2.rpc.call('sendinvoice', {'offer': offer['bolt12'],
'label': 'test sendinvoice 3'})
assert out['label'] == 'test sendinvoice 3'
assert out['description'] == 'simple test'
assert 'issuer' not in out
assert 'bolt12' in out
assert 'payment_hash' in out
assert out['status'] == 'paid'
assert 'payment_preimage' in out
assert 'expires_at' in out
assert out['amount_msat'] == Millisatoshi(10000000)
assert 'pay_index' in out
assert out['amount_received_msat'] == Millisatoshi(10000000)
def test_self_pay(node_factory):
"""Repro test for issue 4345: pay ourselves via the pay plugin.