diff --git a/plugins/Makefile b/plugins/Makefile index 4bfee3e1e..d56827285 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -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) diff --git a/plugins/fetchinvoice.c b/plugins/fetchinvoice.c index f571634f2..bd581615b 100644 --- a/plugins/fetchinvoice.c +++ b/plugins/fetchinvoice.c @@ -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", diff --git a/plugins/offers.c b/plugins/offers.c index ebd7c8d64..040b7f038 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -14,7 +14,6 @@ #include #include #include -#include #include #include @@ -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", diff --git a/plugins/offers_inv_hook.c b/plugins/offers_inv_hook.c deleted file mode 100644 index 67816e8e8..000000000 --- a/plugins/offers_inv_hook.c +++ /dev/null @@ -1,401 +0,0 @@ -#include "config.h" -#include -#include -#include -#include -#include -#include -#include -#include - -/* 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); -} - diff --git a/plugins/offers_inv_hook.h b/plugins/offers_inv_hook.h deleted file mode 100644 index 698037391..000000000 --- a/plugins/offers_inv_hook.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H -#define LIGHTNING_PLUGINS_OFFERS_INV_HOOK_H -#include "config.h" -#include - -/* 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 */ diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index 51da8a536..55a9c0356 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -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); -} - diff --git a/tests/test_pay.py b/tests/test_pay.py index ca1d67ae6..5cd79830f 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -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.