diff --git a/plugins/Makefile b/plugins/Makefile index 136015ab8..3c23564e8 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -25,9 +25,9 @@ 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 +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 := plugins/offers_offer.h +PLUGIN_OFFERS_HEADER := plugins/offers_offer.h plugins/offers_invreq_hook.h PLUGIN_SPENDER_SRC := \ plugins/spender/fundchannel.c \ diff --git a/plugins/offers.c b/plugins/offers.c index bb42e35dd..766d83f84 100644 --- a/plugins/offers.c +++ b/plugins/offers.c @@ -1,11 +1,41 @@ /* This plugin covers both sending and receiving offers */ #include #include +#include #include struct pubkey32 id; +u32 cltv_final; + +static struct command_result *onion_message_call(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + const jsmntok_t *om, *invreqtok; + + om = json_get_member(buf, params, "onion_message"); + + invreqtok = json_get_member(buf, om, "invoice_request"); + if (invreqtok) { + const jsmntok_t *replytok; + + replytok = json_get_member(buf, om, "reply_path"); + if (replytok && replytok->size > 0) + return handle_invoice_request(cmd, buf, + invreqtok, replytok); + else + plugin_log(cmd->plugin, LOG_DBG, + "invoice_request without reply_path"); + } + + return command_hook_success(cmd); +} static const struct plugin_hook hooks[] = { + { + "onion_message", + onion_message_call + }, }; static void init(struct plugin *p, @@ -24,6 +54,12 @@ static void init(struct plugin *p, if (secp256k1_xonly_pubkey_from_pubkey(secp256k1_ctx, &id.pubkey, NULL, &k.pubkey) != 1) abort(); + + field = + rpc_delve(tmpctx, p, "listconfigs", + take(json_out_obj(NULL, "config", "cltv-final")), + ".cltv-final"); + cltv_final = atoi(field); } static const struct plugin_command commands[] = { @@ -40,6 +76,8 @@ int main(int argc, char *argv[]) { setup_locale(); + /* We deal in UTC; mktime() uses local time */ + setenv("TZ", "", 1); plugin_main(argv, init, PLUGIN_RESTARTABLE, true, NULL, commands, ARRAY_SIZE(commands), NULL, 0, hooks, ARRAY_SIZE(hooks), NULL); diff --git a/plugins/offers_invreq_hook.c b/plugins/offers_invreq_hook.c new file mode 100644 index 000000000..c2962e290 --- /dev/null +++ b/plugins/offers_invreq_hook.c @@ -0,0 +1,762 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* We need to keep the reply path around so we can reply with invoice */ +struct invreq { + struct tlv_invoice_request *invreq; + const char *buf; + const jsmntok_t *replytok; + + /* The offer, once we've looked it up. */ + struct tlv_offer *offer; + + /* The invoice we're preparing (can require additional lookups) */ + struct tlv_invoice *inv; + + /* The preimage for the invoice. */ + struct preimage preimage; +}; + +static struct command_result *WARN_UNUSED_RESULT +fail_invreq_level(struct command *cmd, + const struct invreq *invreq, + enum log_level l, + const char *fmt, va_list ap) +{ + char *full_fmt, *msg; + + full_fmt = tal_fmt(tmpctx, "Failed invoice_request %s", + invrequest_encode(tmpctx, invreq->invreq)); + if (invreq->invreq->offer_id) + tal_append_fmt(&full_fmt, " for offer %s", + type_to_string(tmpctx, struct sha256, + invreq->invreq->offer_id)); + tal_append_fmt(&full_fmt, ": %s", fmt); + + msg = tal_vfmt(tmpctx, full_fmt, ap); + plugin_log(cmd->plugin, l, "%s", msg); + + /* FIXME: send reply */ + return command_hook_success(cmd); +} + +static struct command_result *WARN_UNUSED_RESULT +fail_invreq(struct command *cmd, + const struct invreq *invreq, + const char *fmt, ...) +{ + va_list ap; + struct command_result *ret; + + va_start(ap, fmt); + ret = fail_invreq_level(cmd, invreq, LOG_DBG, fmt, ap); + va_end(ap); + + return ret; +} + +static struct command_result *WARN_UNUSED_RESULT +fail_internalerr(struct command *cmd, + const struct invreq *invreq, + const char *fmt, ...) +{ + va_list ap; + struct command_result *ret; + + va_start(ap, fmt); + ret = fail_invreq_level(cmd, invreq, LOG_BROKEN, fmt, ap); + va_end(ap); + + return ret; +} + +#define invreq_must_have(cmd_, ir_, fld_) \ + test_field(cmd_, ir_, ir_->invreq->fld_ != NULL, #fld_, "missing") +#define invreq_must_not_have(cmd_, ir_, fld_) \ + test_field(cmd_, ir_, ir_->invreq->fld_ == NULL, #fld_, "unexpected") + +static struct command_result * +test_field(struct command *cmd, + const struct invreq *invreq, + bool test, const char *fieldname, const char *what) +{ + if (!test) + return fail_invreq(cmd, invreq, "%s %s", what, fieldname); + return NULL; +} + +/* BOLT-offers #12: + * - if the invoice corresponds to an offer with `recurrence`: + * ... + * - if it sets `relative_expiry`: + * - MUST NOT set `relative_expiry` `seconds_from_timestamp` more than the + * number of seconds after `timestamp` that payment for this period will + * be accepted. + */ +static void set_recurring_inv_expiry(struct tlv_invoice *inv, u64 last_pay) +{ + inv->relative_expiry = tal(inv, u32); + + /* Don't give them a 0 second invoice, even if it's true. */ + if (last_pay <= *inv->timestamp) + *inv->relative_expiry = 1; + else + *inv->relative_expiry = last_pay - *inv->timestamp; + + /* FIXME: Shorten expiry if we're doing currency conversion! */ +} + +/* We rely on label forms for uniqueness. */ +static void json_add_label(struct json_stream *js, + const struct sha256 *offer_id, + const struct pubkey32 *payer_key, + const u32 counter) +{ + char *label; + + label = tal_fmt(tmpctx, "%s-%s-%u", + type_to_string(tmpctx, struct sha256, offer_id), + type_to_string(tmpctx, struct pubkey32, + payer_key), + counter); + json_add_string(js, "label", label); +} + +static struct command_result *finished(struct command *cmd, + const char *buf, + const jsmntok_t *result, + void *unused) +{ + return command_hook_success(cmd); +} + +/* Note: this can actually happen if a single-use offer is already + * used at the same time between the check and now. + */ +static struct command_result *error(struct command *cmd, + const char *buf, + const jsmntok_t *err, + struct invreq *ir) +{ + return fail_internalerr(cmd, ir, + "Got JSON error: %.*s", + json_tok_full_len(err), + json_tok_full(buf, err)); +} + +static struct command_result *createinvoice_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct invreq *ir) +{ + char *hrp; + u8 *rawinv; + struct out_req *req; + size_t i; + const jsmntok_t *t; + + /* We have a signed invoice, use it as a reply. */ + t = json_get_member(buf, result, "bolt12"); + if (!from_bech32_charset(tmpctx, buf + t->start, t->end - t->start, + &hrp, &rawinv)) { + return fail_internalerr(cmd, ir, + "Bad creatinvoice bolt12 string %.*s", + json_tok_full_len(t), + json_tok_full(buf, t)); + } + + /* Now, send invoice to requester, using return route. */ + req = jsonrpc_request_start(cmd->plugin, cmd, "sendonionmessage", + finished, error, ir); + + /* Add invoice into last hop. */ + json_array_start(req->js, "hops"); + json_for_each_arr(i, t, ir->replytok) { + size_t j; + const jsmntok_t *t2; + + json_object_start(req->js, NULL); + json_for_each_obj(j, t2, t) + json_add_tok(req->js, + json_strdup(tmpctx, ir->buf, t2), + t2+1, ir->buf); + if (i == ir->replytok->size - 1) + json_add_hex_talarr(req->js, "invoice", rawinv); + json_object_end(req->js); + } + json_array_end(req->js); + return send_outreq(cmd->plugin, req); +} + +static struct command_result *create_invoicereq(struct command *cmd, + struct invreq *ir) +{ + struct out_req *req; + + /* Now, write invoice to db (returns the signed version) */ + req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoice", + createinvoice_done, error, ir); + + json_add_string(req->js, "invstring", invoice_encode(tmpctx, ir->inv)); + json_add_preimage(req->js, "preimage", &ir->preimage); + json_add_label(req->js, ir->inv->offer_id, ir->inv->payer_key, + ir->inv->recurrence_counter + ? *ir->inv->recurrence_counter : 0); + return send_outreq(cmd->plugin, req); +} + +static struct command_result *check_period(struct command *cmd, + struct invreq *ir, + u64 basetime) +{ + u64 period_idx; + u64 paywindow_start, paywindow_end; + struct command_result *err; + + /* If we have a recurrence base, that overrides. */ + if (ir->offer->recurrence_base) + basetime = ir->offer->recurrence_base->basetime; + + /* BOLT-offers #12: + * - if the invoice corresponds to an offer with `recurrence`: + * - MUST set `recurrence_basetime` to the start of period #0 as + * calculated by [Period Calculation](#offer-period-calculation). + */ + ir->inv->recurrence_basetime = tal_dup(ir->inv, u64, &basetime); + + period_idx = *ir->invreq->recurrence_counter; + + /* BOLT-offers #12: + * - if the offer had `recurrence_base` and `start_any_period` + * was 1: + * - MUST fail the request if there is no `recurrence_start` + * field. + * - MUST consider the period index for this request to be the + * `recurrence_start` field plus the `recurrence_counter` + * `counter` field. + */ + if (ir->offer->recurrence_base + && ir->offer->recurrence_base->start_any_period) { + err = invreq_must_have(cmd, ir, recurrence_start); + if (err) + return err; + period_idx += *ir->invreq->recurrence_start; + } else { + /* BOLT-offers #12: + * + * - otherwise: + * - MUST fail the request if there is a `recurrence_start` + * field. + * - MUST consider the period index for this request to be the + * `recurrence_counter` `counter` field. + */ + err = invreq_must_not_have(cmd, ir, recurrence_start); + if (err) + return err; + } + + /* BOLT-offers #12: + * - if the offer has a `recurrence_limit`: + * - MUST fail the request if the period index is greater than + * `max_period`. + */ + if (ir->offer->recurrence_limit + && period_idx > *ir->offer->recurrence_limit) { + return fail_invreq(cmd, ir, + "period_index %"PRIu64" too great", + period_idx); + } + + offer_period_paywindow(ir->offer->recurrence, + ir->offer->recurrence_paywindow, + ir->offer->recurrence_base, + basetime, period_idx, + &paywindow_start, &paywindow_end); + if (*ir->inv->timestamp < paywindow_start) { + return fail_invreq(cmd, ir, + "period_index %"PRIu64 + " too early (start %"PRIu64")", + period_idx, + paywindow_start); + } + if (*ir->inv->timestamp > paywindow_end) { + return fail_invreq(cmd, ir, + "period_index %"PRIu64 + " too late (ended %"PRIu64")", + period_idx, + paywindow_end); + } + + set_recurring_inv_expiry(ir->inv, paywindow_end); + + /* BOLT-offers #12: + * + * - if `recurrence_counter` is non-zero: + *... + * - if the offer had a `recurrence_paywindow`: + *... + * - if `proportional_amount` is 1: + * - MUST adjust the *base invoice amount* proportional to time + * remaining in the period. + */ + if (*ir->invreq->recurrence_counter != 0 + && ir->offer->recurrence_paywindow + && ir->offer->recurrence_paywindow->proportional_amount == 1) { + u64 start = offer_period_start(basetime, period_idx, + ir->offer->recurrence); + u64 end = offer_period_start(basetime, period_idx + 1, + ir->offer->recurrence); + + if (*ir->inv->timestamp > start) { + *ir->inv->amount + *= (double)((*ir->inv->timestamp - start) + / (end - start)); + /* Round up to make it non-zero if necessary. */ + if (*ir->inv->amount == 0) + *ir->inv->amount = 1; + } + } + + return create_invoicereq(cmd, ir); +} + +static struct command_result *prev_invoice_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct invreq *ir) +{ + const jsmntok_t *status, *arr, *b12; + struct tlv_invoice *previnv; + char *fail; + + /* Was it created? */ + arr = json_get_member(buf, result, "invoices"); + if (arr->size == 0) { + return fail_invreq(cmd, ir, + "No previous invoice #%u", + *ir->inv->recurrence_counter - 1); + } + + /* Was it paid? */ + status = json_get_member(buf, arr + 1, "status"); + if (!json_tok_streq(buf, status, "paid")) { + return fail_invreq(cmd, ir, + "Previous invoice #%u status *.%s", + *ir->inv->recurrence_counter - 1, + json_tok_full_len(status), + json_tok_full(buf, status)); + } + + /* Decode it */ + b12 = json_get_member(buf, arr + 1, "bolt12"); + if (!b12) { + return fail_internalerr(cmd, ir, + "Previous invoice #%u no bolt12 (%.*s)", + *ir->inv->recurrence_counter - 1, + json_tok_full_len(arr + 1), + json_tok_full(buf, arr + 1)); + } + previnv = invoice_decode(tmpctx, buf + b12->start, b12->end - b12->start, + plugin_feature_set(cmd->plugin), + chainparams, &fail); + if (!previnv) { + return fail_internalerr(cmd, ir, + "Previous invoice %.*s can't decode?", + json_tok_full_len(b12), + json_tok_full(buf, b12)); + } + if (!previnv->recurrence_basetime) { + return fail_internalerr(cmd, ir, + "Previous invoice %.*s no recurrence_basetime?", + json_tok_full_len(b12), json_tok_full(buf, b12)); + } + return check_period(cmd, ir, *previnv->recurrence_basetime); +} + +/* Now, we need to check the previous invoice was paid, and maybe get timebase */ +static struct command_result *check_previous_invoice(struct command *cmd, + struct invreq *ir) +{ + struct out_req *req; + + /* No previous? Just pass through */ + if (*ir->invreq->recurrence_counter == 0) + return check_period(cmd, ir, *ir->inv->timestamp); + + req = jsonrpc_request_start(cmd->plugin, cmd, + "listinvoices", + prev_invoice_done, + error, + ir); + json_add_label(req->js, + ir->invreq->offer_id, + ir->invreq->payer_key, + *ir->invreq->recurrence_counter - 1); + return send_outreq(cmd->plugin, req); +} + +/* BOLT-offers #12: + * - MUST fail the request if `recurrence_signature` is not correct. + */ +static bool check_recurrence_sig(const struct tlv_invoice_request *invreq, + const struct pubkey32 *payer_key, + const struct bip340sig *sig) +{ + struct sha256 merkle, sighash; + merkle_tlv(invreq->fields, &merkle); + sighash_from_merkle("invoice_request", "recurrence_signature", + &merkle, &sighash); + + return secp256k1_schnorrsig_verify(secp256k1_ctx, + sig->u8, + sighash.u.u8, &payer_key->pubkey) == 1; +} + +static struct command_result *listoffers_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct invreq *ir) +{ + 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 command_result *err; + + /* BOLT-offers #12: + * + * - MUST fail the request if the `offer_id` does not refer to an + * unexpired offer. + */ + if (arr->size == 0) + return fail_invreq(cmd, ir, "Unknown offer"); + + offertok = arr + 1; + + activetok = json_get_member(buf, offertok, "active"); + if (!activetok) { + return fail_internalerr(cmd, ir, + "Missing active: %.*s", + json_tok_full_len(offertok), + json_tok_full(buf, offertok)); + } + json_to_bool(buf, activetok, &active); + if (!active) + return fail_invreq(cmd, ir, "Offer no longer available"); + + b12tok = json_get_member(buf, offertok, "bolt12"); + if (!b12tok) { + return fail_internalerr(cmd, ir, + "Missing bolt12: %.*s", + json_tok_full_len(offertok), + json_tok_full(buf, offertok)); + } + ir->offer = offer_decode(ir, + buf + b12tok->start, + b12tok->end - b12tok->start, + plugin_feature_set(cmd->plugin), + chainparams, &fail); + if (!ir->offer) { + return fail_internalerr(cmd, ir, + "Invalid offer: %s (%.*s)", + fail, + json_tok_full_len(offertok), + json_tok_full(buf, offertok)); + } + + if (ir->offer->absolute_expiry + && time_now().ts.tv_sec >= *ir->offer->absolute_expiry) { + /* FIXME: do deloffer to disable it */ + return fail_invreq(cmd, ir, "Offer expired"); + } + + /* 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 (ir->offer->quantity_min || ir->offer->quantity_max) { + err = invreq_must_have(cmd, ir, quantity); + if (err) + return err; + + if (ir->offer->quantity_min && + *ir->invreq->quantity < *ir->offer->quantity_min) { + return fail_invreq(cmd, ir, + "quantity %"PRIu64 " < %"PRIu64, + *ir->invreq->quantity, + *ir->offer->quantity_min); + } + + if (ir->offer->quantity_max && + *ir->invreq->quantity > *ir->offer->quantity_max) { + return fail_invreq(cmd, ir, + "quantity %"PRIu64" > %"PRIu64, + *ir->invreq->quantity, + *ir->offer->quantity_max); + } + } else { + err = invreq_must_not_have(cmd, ir, quantity); + if (err) + return err; + } + + if (ir->offer->amount) { + u64 raw_amount; + + /* BOLT-offers #12: + * + * - if the offer included `amount`: + * - MUST fail the request if it contains `amount`. + */ + err = invreq_must_not_have(cmd, ir, amount); + if (err) + return err; + + + /* BOLT-offers #12: + * - MUST calculate the *base invoice amount* using the offer + * `amount`: + * - if offer `currency` is not the invoice currency, convert + * to the invoice currency. + */ + if (ir->offer->currency) { + /* FIXME: Currency conversion! */ + return fail_invreq(cmd, ir, + "FIXME: Request for currency %.*s", + (int)tal_bytelen(ir->offer->currency), + (char *)ir->offer->currency); + } else + raw_amount = *ir->offer->amount; + + /* BOLT-offers #12: + * - if request contains `quantity`, multiply by `quantity`. + */ + if (ir->invreq->quantity) { + if (mul_overflows_u64(*ir->invreq->quantity, raw_amount)) { + return fail_invreq(cmd, ir, + "quantity %"PRIu64 + " causes overflow", + *ir->invreq->quantity); + } + raw_amount *= *ir->invreq->quantity; + } + + amt = amount_msat(raw_amount); + } else { + /* BOLT-offers #12: + * + * - otherwise: + * - MUST fail the request if it does not contain `amount`. + * - MUST use the request `amount` as the *base invoice amount*. + * (Note: invoice amount can be further modiifed by recurrence + * below) + */ + err = invreq_must_have(cmd, ir, amount); + if (err) + return err; + + amt = amount_msat(*ir->invreq->amount); + } + + if (ir->offer->recurrence) { + /* BOLT-offers #12: + * + * - if the offer had a `recurrence`: + * - MUST fail the request if there is no `recurrence_counter` + * field. + * - MUST fail the request if there is no + * `recurrence_signature` field. + * - MUST fail the request if `recurrence_signature` is not + * correct. + */ + err = invreq_must_have(cmd, ir, recurrence_counter); + if (err) + return err; + + err = invreq_must_have(cmd, ir, recurrence_signature); + if (err) + return err; + + if (!check_recurrence_sig(ir->invreq, + ir->invreq->payer_key, + ir->invreq->recurrence_signature)) { + return fail_invreq(cmd, ir, + "bad recurrence_signature"); + } + } else { + /* BOLT-offers #12: + * - otherwise (the offer had no `recurrence`): + * - MUST fail the request if there is a `recurrence_counter` + * field. + * - MUST fail the request if there is a `recurrence_signature` + * field. + */ + err = invreq_must_not_have(cmd, ir, recurrence_counter); + if (err) + return err; + err = invreq_must_not_have(cmd, ir, recurrence_signature); + if (err) + return err; + } + + ir->inv = tlv_invoice_new(cmd); + /* BOLT-offers #12: + * - if the chain for the invoice is not solely bitcoin: + * - MUST specify `chains` the offer is valid for. + */ + if (!streq(chainparams->network_name, "bitcoin")) { + ir->inv->chains = tal_arr(ir->inv, struct bitcoin_blkid, 1); + ir->inv->chains[0] = chainparams->genesis_blockhash; + } + /* BOLT-offers #12: + * - MUST set `offer_id` to the id of the offer. + */ + /* Which is the same as the invreq */ + ir->inv->offer_id = tal_dup(ir->inv, struct sha256, + ir->invreq->offer_id); + ir->inv->amount = tal_dup(ir->inv, u64, + &amt.millisatoshis); /* Raw: wire protocol */ + ir->inv->description = tal_dup_talarr(ir->inv, char, + ir->offer->description); + ir->inv->features = tal_dup_talarr(ir->inv, u8, + plugin_feature_set(cmd->plugin) + ->bits[BOLT11_FEATURE]); + /* FIXME: Insert paths and payinfo */ + + ir->inv->vendor = tal_dup_talarr(ir->inv, char, ir->offer->vendor); + ir->inv->node_id = tal_dup(ir->inv, struct pubkey32, ir->offer->node_id); + /* BOLT-offers #12: + * - MUST set (or not set) `quantity` exactly as the invoice_request + * did. + */ + if (ir->offer->quantity_min || ir->offer->quantity_max) + ir->inv->quantity = tal_dup(ir->inv, u64, ir->invreq->quantity); + /* BOLT-offers #12: + * - MUST set `payer_key` exactly as the invoice_request did. + */ + ir->inv->payer_key = tal_dup(ir->inv, struct pubkey32, + ir->invreq->payer_key); + + /* BOLT-offers #12: + * - MUST set (or not set) `payer_info` exactly as the invoice_request + * did. + */ + ir->inv->payer_info + = tal_dup_talarr(ir->inv, u8, ir->invreq->payer_info); + + randombytes_buf(&ir->preimage, sizeof(ir->preimage)); + ir->inv->payment_hash = tal(ir->inv, struct sha256); + sha256(ir->inv->payment_hash, &ir->preimage, sizeof(ir->preimage)); + + ir->inv->cltv = tal_dup(ir->inv, u32, &cltv_final); + + ir->inv->timestamp = tal(ir->inv, u64); + *ir->inv->timestamp = time_now().ts.tv_sec; + + /* Last of all, we handle recurrence details, which often requires + * further lookups. */ + + /* BOLT-offers #12: + * - MUST set (or not set) `recurrence_counter` exactly as the + * invoice_request did. + */ + if (ir->invreq->recurrence_counter) { + ir->inv->recurrence_counter = ir->invreq->recurrence_counter; + return check_previous_invoice(cmd, ir); + } + /* We're happy with 2 hours timeout (default): they can always + * request another. */ + + /* FIXME: Fallbacks? */ + /* FIXME: refunds? */ + return create_invoicereq(cmd, ir); +} + +static struct command_result *handle_offerless_request(struct command *cmd, + struct invreq *ir) +{ + /* FIXME: shut up and take their money! */ + return fail_internalerr(cmd, ir, "FIXME: handle offerless req!"); +} + +struct command_result *handle_invoice_request(struct command *cmd, + const char *buf, + const jsmntok_t *invreqtok, + const jsmntok_t *replytok) +{ + const u8 *invreqbin = json_tok_bin_from_hex(cmd, buf, invreqtok); + size_t len = tal_count(invreqbin); + struct invreq *ir = tal(cmd, struct invreq); + struct out_req *req; + int bad_feature; + + /* Make a copy of entire buffer, for later. */ + ir->buf = tal_dup_arr(ir, char, buf, replytok->end, 0); + ir->replytok = replytok; + + ir->invreq = tlv_invoice_request_new(cmd); + if (!fromwire_invoice_request(&invreqbin, &len, ir->invreq)) { + return fail_invreq(cmd, ir, + "Invalid invreq %s", + tal_hex(tmpctx, invreqbin)); + } + + /* 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), + ir->invreq->features, + BOLT11_FEATURE); + if (bad_feature != -1) { + return fail_invreq(cmd, ir, + "Unsupported invreq feature %i", + bad_feature); + } + + /* BOLT-offers #12: + * + * The reader of an invoice_request: + *... + * - MUST fail the request if `chains` does not include (or imply) a + * supported chain. + */ + if (!bolt12_chains_match(ir->invreq->chains, chainparams)) { + return fail_invreq(cmd, ir, + "Wrong chains %s", + tal_hex(tmpctx, ir->invreq->chains)); + } + + /* BOLT-offers #12: + * + * The reader of an invoice_request: + * - MUST fail the request if `payer_key` is not present. + */ + if (!ir->invreq->payer_key) + return fail_invreq(cmd, ir, "Missing payer key"); + + if (!ir->invreq->offer_id) + return handle_offerless_request(cmd, ir); + + /* Now, look up offer */ + req = jsonrpc_request_start(cmd->plugin, cmd, "listoffers", + listoffers_done, error, ir); + json_add_sha256(req->js, "offer_id", ir->invreq->offer_id); + return send_outreq(cmd->plugin, req); +} + diff --git a/plugins/offers_invreq_hook.h b/plugins/offers_invreq_hook.h new file mode 100644 index 000000000..a3a7965a3 --- /dev/null +++ b/plugins/offers_invreq_hook.h @@ -0,0 +1,13 @@ +#ifndef LIGHTNING_PLUGINS_OFFERS_INVREQ_HOOK_H +#define LIGHTNING_PLUGINS_OFFERS_INVREQ_HOOK_H +#include "config.h" +#include + +extern u32 cltv_final; + +/* We got an onionmessage with an invreq! */ +struct command_result *handle_invoice_request(struct command *cmd, + const char *buf, + const jsmntok_t *invreqtok, + const jsmntok_t *replytok); +#endif /* LIGHTNING_PLUGINS_OFFERS_INVREQ_HOOK_H */