mirror of
https://github.com/ElementsProject/lightning.git
synced 2025-02-20 13:54:36 +01:00
lightningd: re-add 'offerout' functionality, as 'invoicerequest'.
Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
parent
7906770489
commit
37bc4603b8
11 changed files with 907 additions and 28 deletions
|
@ -349,10 +349,10 @@ static bool print_recurrence_counter_with_base(const u32 *recurrence_counter,
|
|||
return true;
|
||||
}
|
||||
|
||||
static void print_payment_hash(const struct sha256 *payment_hash)
|
||||
static void print_hash(const char *fieldname, const struct sha256 *hash)
|
||||
{
|
||||
printf("invoice_payment_hash: %s\n",
|
||||
type_to_string(tmpctx, struct sha256, payment_hash));
|
||||
printf("%s: %s\n",
|
||||
fieldname, type_to_string(tmpctx, struct sha256, hash));
|
||||
}
|
||||
|
||||
static void print_relative_expiry(u64 *created_at, u32 *relative)
|
||||
|
@ -511,12 +511,15 @@ int main(int argc, char *argv[])
|
|||
}
|
||||
|
||||
if (streq(hrp, "lno")) {
|
||||
struct sha256 offer_id;
|
||||
const struct tlv_offer *offer
|
||||
= offer_decode(ctx, argv[2], strlen(argv[2]),
|
||||
NULL, NULL, &fail);
|
||||
if (!offer)
|
||||
errx(ERROR_BAD_DECODE, "Bad offer: %s", fail);
|
||||
|
||||
offer_offer_id(offer, &offer_id);
|
||||
print_hash("offer_id", &offer_id);
|
||||
if (offer->offer_chains)
|
||||
print_offer_chains(offer->offer_chains);
|
||||
if (offer->offer_amount)
|
||||
|
@ -545,12 +548,20 @@ int main(int argc, char *argv[])
|
|||
if (!print_extra_fields(offer->fields))
|
||||
well_formed = false;
|
||||
} else if (streq(hrp, "lnr")) {
|
||||
struct sha256 offer_id, invreq_id;
|
||||
const struct tlv_invoice_request *invreq
|
||||
= invrequest_decode(ctx, argv[2], strlen(argv[2]),
|
||||
NULL, NULL, &fail);
|
||||
if (!invreq)
|
||||
errx(ERROR_BAD_DECODE, "Bad invreq: %s", fail);
|
||||
|
||||
if (invreq->offer_node_id) {
|
||||
invreq_offer_id(invreq, &offer_id);
|
||||
print_hash("offer_id", &offer_id);
|
||||
}
|
||||
invreq_invreq_id(invreq, &invreq_id);
|
||||
print_hash("invreq_id", &invreq_id);
|
||||
|
||||
/* FIXME: We can do more intra-field checking! */
|
||||
if (must_have(invreq, invreq_metadata))
|
||||
print_hex("invreq_metadata", invreq->invreq_metadata);
|
||||
|
@ -572,7 +583,7 @@ int main(int argc, char *argv[])
|
|||
well_formed &= print_utf8("offer_issuer", invreq->offer_issuer);
|
||||
if (invreq->offer_quantity_max)
|
||||
print_u64("offer_quantity_max", *invreq->offer_quantity_max);
|
||||
if (must_have(invreq, offer_node_id))
|
||||
if (invreq->offer_node_id)
|
||||
print_node_id("offer_node_id", invreq->offer_node_id);
|
||||
if (invreq->offer_recurrence)
|
||||
well_formed &= print_recurrance(invreq->offer_recurrence,
|
||||
|
@ -607,12 +618,22 @@ int main(int argc, char *argv[])
|
|||
if (!print_extra_fields(invreq->fields))
|
||||
well_formed = false;
|
||||
} else if (streq(hrp, "lni")) {
|
||||
struct sha256 offer_id, invreq_id;
|
||||
const struct tlv_invoice *invoice
|
||||
= invoice_decode(ctx, argv[2], strlen(argv[2]),
|
||||
NULL, NULL, &fail);
|
||||
if (!invoice)
|
||||
errx(ERROR_BAD_DECODE, "Bad invoice: %s", fail);
|
||||
|
||||
if (invoice->invreq_payer_id) {
|
||||
if (invoice->offer_node_id) {
|
||||
invoice_offer_id(invoice, &offer_id);
|
||||
print_hash("offer_id", &offer_id);
|
||||
}
|
||||
invoice_invreq_id(invoice, &invreq_id);
|
||||
print_hash("invreq_id", &invreq_id);
|
||||
}
|
||||
|
||||
/* FIXME: We can do more intra-field checking! */
|
||||
if (must_have(invoice, invreq_metadata))
|
||||
print_hex("invreq_metadata", invoice->invreq_metadata);
|
||||
|
@ -670,7 +691,7 @@ int main(int argc, char *argv[])
|
|||
print_relative_expiry(invoice->invoice_created_at,
|
||||
invoice->invoice_relative_expiry);
|
||||
if (must_have(invoice, invoice_payment_hash))
|
||||
print_payment_hash(invoice->invoice_payment_hash);
|
||||
print_hash("invoice_payment_hash", invoice->invoice_payment_hash);
|
||||
if (must_have(invoice, invoice_amount))
|
||||
print_msat("invoice_amount", *invoice->invoice_amount);
|
||||
if (invoice->invoice_fallbacks)
|
||||
|
|
|
@ -396,7 +396,7 @@ static struct command_result *json_createinvoicerequest(struct command *cmd,
|
|||
struct json_stream *response;
|
||||
u64 *prev_basetime = NULL;
|
||||
struct sha256 merkle;
|
||||
bool *save, *single_use;
|
||||
bool *save, *single_use, *exposeid;
|
||||
enum offer_status status;
|
||||
struct sha256 invreq_id;
|
||||
const char *b12str;
|
||||
|
@ -404,6 +404,7 @@ static struct command_result *json_createinvoicerequest(struct command *cmd,
|
|||
if (!param(cmd, buffer, params,
|
||||
p_req("bolt12", param_b12_invreq, &invreq),
|
||||
p_req("savetodb", param_bool, &save),
|
||||
p_opt_def("exposeid", param_bool, &exposeid, false),
|
||||
p_opt("recurrence_label", param_label, &label),
|
||||
p_opt_def("single_use", param_bool, &single_use, true),
|
||||
NULL))
|
||||
|
@ -449,10 +450,14 @@ static struct command_result *json_createinvoicerequest(struct command *cmd,
|
|||
}
|
||||
|
||||
invreq->invreq_payer_id = tal(invreq, struct pubkey);
|
||||
if (!payer_key(cmd->ld,
|
||||
invreq->invreq_metadata,
|
||||
tal_bytelen(invreq->invreq_metadata),
|
||||
invreq->invreq_payer_id)) {
|
||||
if (*exposeid) {
|
||||
if (!pubkey_from_node_id(invreq->invreq_payer_id,
|
||||
&cmd->ld->id))
|
||||
fatal("Our ID is invalid?");
|
||||
} else if (!payer_key(cmd->ld,
|
||||
invreq->invreq_metadata,
|
||||
tal_bytelen(invreq->invreq_metadata),
|
||||
invreq->invreq_payer_id)) {
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"Invalid tweak");
|
||||
}
|
||||
|
@ -466,8 +471,8 @@ static struct command_result *json_createinvoicerequest(struct command *cmd,
|
|||
merkle_tlv(invreq->fields, &merkle);
|
||||
invreq->signature = tal(invreq, struct bip340sig);
|
||||
hsm_sign_b12(cmd->ld, "invoice_request", "signature",
|
||||
&merkle, invreq->invreq_metadata, invreq->invreq_payer_id,
|
||||
invreq->signature);
|
||||
&merkle, *exposeid ? NULL : invreq->invreq_metadata,
|
||||
invreq->invreq_payer_id, invreq->signature);
|
||||
|
||||
b12str = invrequest_encode(cmd, invreq);
|
||||
|
||||
|
|
|
@ -31,13 +31,13 @@ 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
|
||||
PLUGIN_OFFERS_SRC := plugins/offers.c plugins/offers_offer.c plugins/offers_invreq_hook.c plugins/offers_inv_hook.c
|
||||
PLUGIN_OFFERS_OBJS := $(PLUGIN_OFFERS_SRC:.c=.o)
|
||||
PLUGIN_OFFERS_HEADER := $(PLUGIN_OFFERS_SRC:.c=.h)
|
||||
|
||||
PLUGIN_FETCHINVOICE_SRC := plugins/fetchinvoice.c
|
||||
PLUGIN_FETCHINVOICE_OBJS := $(PLUGIN_FETCHINVOICE_SRC:.c=.o)
|
||||
PLUGIN_FETCHINVOICE_HEADER :=
|
||||
PLUGIN_FETCHINVOICE_HEADER :=
|
||||
|
||||
PLUGIN_SPENDER_SRC := \
|
||||
plugins/spender/fundchannel.c \
|
||||
|
|
|
@ -707,6 +707,32 @@ 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,
|
||||
|
@ -944,7 +970,7 @@ force_payer_secret(struct command *cmd,
|
|||
}
|
||||
|
||||
sent->path = path_to_node(sent, cmd->plugin,
|
||||
sent->invreq->offer_node_id);
|
||||
sent->invreq->invreq_payer_id);
|
||||
if (!sent->path)
|
||||
return connect_direct(cmd, sent->invreq->offer_node_id,
|
||||
sendinvreq_after_connect, sent);
|
||||
|
@ -1198,7 +1224,83 @@ static struct command_result *invoice_payment(struct command *cmd,
|
|||
return command_hook_success(cmd);
|
||||
}
|
||||
|
||||
#if DEVELOPER
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - if it sends an invoice in response:
|
||||
* - MUST use `offer_paths` if present, otherwise MUST use
|
||||
* `invreq_payer_id` as the node id to send to.
|
||||
*/
|
||||
/* FIXME! */
|
||||
if (sent->invreq->offer_paths) {
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"FIXME: support blinded paths!");
|
||||
}
|
||||
|
||||
sent->path = path_to_node(sent, cmd->plugin,
|
||||
sent->invreq->invreq_payer_id);
|
||||
if (!sent->path)
|
||||
return connect_direct(cmd, sent->invreq->invreq_payer_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 struct command_result *param_invreq(struct command *cmd,
|
||||
const char *name,
|
||||
const char *buffer,
|
||||
|
@ -1206,6 +1308,231 @@ static struct command_result *param_invreq(struct command *cmd,
|
|||
struct tlv_invoice_request **invreq)
|
||||
{
|
||||
char *fail;
|
||||
int badf;
|
||||
u8 *wire;
|
||||
struct sha256 merkle, sighash;
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - if `invreq_chain` is not present:
|
||||
* - MUST fail the request if bitcoin is not a supported chain.
|
||||
* - otherwise:
|
||||
* - MUST fail the request if `invreq_chain`.`chain` is not a
|
||||
* supported chain.
|
||||
*/
|
||||
*invreq = invrequest_decode(cmd,
|
||||
buffer + tok->start, tok->end - tok->start,
|
||||
plugin_feature_set(cmd->plugin),
|
||||
chainparams,
|
||||
&fail);
|
||||
if (!*invreq)
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
tal_fmt(cmd,
|
||||
"Unparsable invoice_request: %s",
|
||||
fail));
|
||||
/* BOLT-offers #12:
|
||||
* The reader:
|
||||
* - MUST fail the request if `invreq_payer_id` or `invreq_metadata`
|
||||
* are not present.
|
||||
* - MUST fail the request if any non-signature TLV fields greater or
|
||||
* equal to 160.
|
||||
* - if `invreq_features` contains unknown _odd_ bits that are
|
||||
* non-zero:
|
||||
* - MUST ignore the bit.
|
||||
* - if `invreq_features` contains unknown _even_ bits that are
|
||||
* non-zero:
|
||||
* - MUST fail the request.
|
||||
*/
|
||||
if (!(*invreq)->invreq_payer_id)
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Missing invreq_payer_id");
|
||||
|
||||
if (!(*invreq)->invreq_metadata)
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Missing invreq_metadata");
|
||||
|
||||
wire = tal_arr(tmpctx, u8, 0);
|
||||
towire_tlv_invoice_request(&wire, *invreq);
|
||||
if (tlv_span(wire, 160, 239, NULL) != 0
|
||||
|| tlv_span(wire, 1001, UINT64_MAX, NULL) != 0) {
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Invalid high-numbered fields");
|
||||
}
|
||||
|
||||
badf = features_unsupported(plugin_feature_set(cmd->plugin),
|
||||
(*invreq)->invreq_features,
|
||||
BOLT12_INVREQ_FEATURE);
|
||||
if (badf != -1) {
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
tal_fmt(tmpctx,
|
||||
"unknown feature %i",
|
||||
badf));
|
||||
}
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - MUST fail the request if `signature` is not correct as detailed in [Signature
|
||||
* Calculation](#signature-calculation) using the `invreq_payer_id`.
|
||||
*/
|
||||
merkle_tlv((*invreq)->fields, &merkle);
|
||||
sighash_from_merkle("invoice_request", "signature", &merkle, &sighash);
|
||||
|
||||
if (!(*invreq)->signature)
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Missing signature");
|
||||
if (!check_schnorr_sig(&sighash,
|
||||
&(*invreq)->invreq_payer_id->pubkey,
|
||||
(*invreq)->signature))
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Invalid signature");
|
||||
|
||||
/* Plugin handles these automatically, you shouldn't send one
|
||||
* manually. */
|
||||
if ((*invreq)->offer_node_id) {
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"This is based on an offer?");
|
||||
}
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - otherwise (no `offer_node_id`, not a response to our offer):
|
||||
* - MUST fail the request if any of the following are present:
|
||||
* - `offer_chains`, `offer_features` or `offer_quantity_max`.
|
||||
* - MUST fail the request if `invreq_amount` is not present.
|
||||
*/
|
||||
if ((*invreq)->offer_chains)
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Unexpected offer_chains");
|
||||
if ((*invreq)->offer_features)
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Unexpected offer_features");
|
||||
if ((*invreq)->offer_quantity_max)
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Unexpected offer_quantity_max");
|
||||
if (!(*invreq)->invreq_amount)
|
||||
return command_fail_badparam(cmd, name, buffer, tok,
|
||||
"Missing invreq_amount");
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - otherwise (no `offer_node_id`, not a response to our offer):
|
||||
*...
|
||||
* - MAY use `offer_amount` (or `offer_currency`) for informational display to user.
|
||||
*/
|
||||
if ((*invreq)->offer_amount && (*invreq)->offer_currency) {
|
||||
plugin_notify_message(cmd, LOG_INFORM,
|
||||
"invoice_request offers %.*s%"PRIu64" as %s",
|
||||
(int)tal_bytelen((*invreq)->offer_currency),
|
||||
(*invreq)->offer_currency,
|
||||
*(*invreq)->offer_amount,
|
||||
fmt_amount_msat(tmpctx,
|
||||
amount_msat(*(*invreq)->invreq_amount)));
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct command_result *json_sendinvoice(struct command *cmd,
|
||||
const char *buffer,
|
||||
const jsmntok_t *params)
|
||||
{
|
||||
struct amount_msat *msat;
|
||||
u32 *timeout;
|
||||
struct sent *sent = tal(cmd, struct sent);
|
||||
|
||||
sent->offer = NULL;
|
||||
sent->cmd = cmd;
|
||||
|
||||
/* FIXME: Support recurring invoice_requests? */
|
||||
if (!param(cmd, buffer, params,
|
||||
p_req("invreq", param_invreq, &sent->invreq),
|
||||
p_req("label", param_label, &sent->inv_label),
|
||||
p_opt("amount_msat", param_msat, &msat),
|
||||
p_opt_def("timeout", param_number, &timeout, 90),
|
||||
NULL))
|
||||
return command_param_failed();
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* The writer:
|
||||
* - MUST copy all non-signature fields from the invreq (including
|
||||
* unknown fields).
|
||||
*/
|
||||
sent->inv = invoice_for_invreq(sent, sent->invreq);
|
||||
|
||||
/* This is how long we'll wait for a reply for. */
|
||||
sent->wait_timeout = *timeout;
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - if `invreq_amount` is present:
|
||||
* - MUST set `invoice_amount` to `invreq_amount`
|
||||
* - otherwise:
|
||||
* - MUST set `invoice_amount` to the *expected amount*.
|
||||
*/
|
||||
if (!msat)
|
||||
sent->inv->invoice_amount = tal_dup(sent->inv, u64,
|
||||
sent->invreq->invreq_amount);
|
||||
else
|
||||
sent->inv->invoice_amount = tal_dup(sent->inv, u64,
|
||||
&msat->millisatoshis); /* Raw: tlv */
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - MUST set `invoice_created_at` to the number of seconds since Midnight 1
|
||||
* January 1970, UTC when the offer was created.
|
||||
* - MUST set `invoice_amount` to the minimum amount it will accept, in units of
|
||||
* the minimal lightning-payable unit (e.g. milli-satoshis for bitcoin) for
|
||||
* `invreq_chain`.
|
||||
*/
|
||||
sent->inv->invoice_created_at = tal(sent->inv, u64);
|
||||
*sent->inv->invoice_created_at = time_now().ts.tv_sec;
|
||||
|
||||
/* FIXME: Support blinded paths, in which case use fake nodeid */
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - MUST set `invoice_payment_hash` to the SHA256 hash of the
|
||||
* `payment_preimage` that will be given in return for payment.
|
||||
*/
|
||||
randombytes_buf(&sent->inv_preimage, sizeof(sent->inv_preimage));
|
||||
sent->inv->invoice_payment_hash = tal(sent->inv, struct sha256);
|
||||
sha256(sent->inv->invoice_payment_hash,
|
||||
&sent->inv_preimage, sizeof(sent->inv_preimage));
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - if `offer_node_id` is present:
|
||||
* - MUST set `invoice_node_id` to `offer_node_id`.
|
||||
* - otherwise:
|
||||
* - MUST set `invoice_node_id` to a valid public key.
|
||||
*/
|
||||
/* FIXME: Use transitory id! */
|
||||
sent->inv->invoice_node_id = tal(sent->inv, struct pubkey);
|
||||
sent->inv->invoice_node_id->pubkey = local_id.pubkey;
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - if the expiry for accepting payment is not 7200 seconds
|
||||
* after `invoice_created_at`:
|
||||
* - MUST set `invoice_relative_expiry`.`seconds_from_creation`
|
||||
* to the number of seconds after `invoice_created_at` that
|
||||
* payment of this invoice should not be attempted.
|
||||
*/
|
||||
if (sent->wait_timeout != 7200) {
|
||||
sent->inv->invoice_relative_expiry = tal(sent->inv, u32);
|
||||
*sent->inv->invoice_relative_expiry = sent->wait_timeout;
|
||||
}
|
||||
|
||||
/* FIXME: recurrence? */
|
||||
if (sent->inv->offer_recurrence)
|
||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||
"FIXME: handle recurring invreq?");
|
||||
|
||||
sent->inv->invoice_features
|
||||
= plugin_feature_set(cmd->plugin)->bits[BOLT12_INVOICE_FEATURE];
|
||||
|
||||
return sign_invoice(cmd, sent);
|
||||
}
|
||||
|
||||
#if DEVELOPER
|
||||
/* This version doesn't do sanity checks! */
|
||||
static struct command_result *param_raw_invreq(struct command *cmd,
|
||||
const char *name,
|
||||
const char *buffer,
|
||||
const jsmntok_t *tok,
|
||||
struct tlv_invoice_request **invreq)
|
||||
{
|
||||
char *fail;
|
||||
|
||||
*invreq = invrequest_decode(cmd, buffer + tok->start, tok->end - tok->start,
|
||||
plugin_feature_set(cmd->plugin), chainparams,
|
||||
|
@ -1227,7 +1554,7 @@ static struct command_result *json_rawrequest(struct command *cmd,
|
|||
struct pubkey *node_id;
|
||||
|
||||
if (!param(cmd, buffer, params,
|
||||
p_req("invreq", param_invreq, &sent->invreq),
|
||||
p_req("invreq", param_raw_invreq, &sent->invreq),
|
||||
p_req("nodeid", param_pubkey, &node_id),
|
||||
p_opt_def("timeout", param_number, &timeout, 60),
|
||||
NULL))
|
||||
|
@ -1256,6 +1583,13 @@ static const struct plugin_command commands[] = {
|
|||
NULL,
|
||||
json_fetchinvoice,
|
||||
},
|
||||
{
|
||||
"sendinvoice",
|
||||
"payment",
|
||||
"Request remote node for to pay this {invreq}, with {label}, optional {amount_msat}, and {timeout} (default 90 seconds).",
|
||||
NULL,
|
||||
json_sendinvoice,
|
||||
},
|
||||
#if DEVELOPER
|
||||
{
|
||||
"dev-rawrequest",
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
#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>
|
||||
|
||||
|
@ -89,7 +90,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;
|
||||
const jsmntok_t *om, *replytok, *invreqtok, *invtok;
|
||||
struct blinded_path *reply_path = NULL;
|
||||
|
||||
if (!offers_enabled)
|
||||
|
@ -117,6 +118,13 @@ 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);
|
||||
}
|
||||
|
||||
|
@ -1070,6 +1078,13 @@ 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
|
||||
},
|
||||
{
|
||||
"invoicerequest",
|
||||
"payment",
|
||||
"Create an invoice_request to send money",
|
||||
"Create an invoice_request to pay invoices of {amount} with {description}, optional {issuer}, internal {label}, and {absolute_expiry}",
|
||||
json_invoicerequest
|
||||
},
|
||||
{
|
||||
"decode",
|
||||
"utility",
|
||||
|
|
326
plugins/offers_inv_hook.c
Normal file
326
plugins/offers_inv_hook.c
Normal file
|
@ -0,0 +1,326 @@
|
|||
#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;
|
||||
struct sha256 invreq_id;
|
||||
|
||||
/* May be NULL */
|
||||
struct blinded_path *reply_path;
|
||||
|
||||
/* The invreq, once we've looked it up. */
|
||||
struct tlv_invoice_request *invreq;
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
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->invoice_amount);
|
||||
|
||||
plugin_log(cmd->plugin, LOG_INFORM,
|
||||
"Payed out %s for invreq %s: %.*s",
|
||||
type_to_string(tmpctx, struct amount_msat, &msat),
|
||||
type_to_string(tmpctx, struct sha256, &inv->invreq_id),
|
||||
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 *listinvreqs_done(struct command *cmd,
|
||||
const char *buf,
|
||||
const jsmntok_t *result,
|
||||
struct inv *inv)
|
||||
{
|
||||
const jsmntok_t *arr = json_get_member(buf, result, "invoicerequests");
|
||||
const jsmntok_t *activetok;
|
||||
bool active;
|
||||
struct amount_msat amt;
|
||||
struct out_req *req;
|
||||
struct sha256 merkle, sighash;
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* A reader of an invoice:
|
||||
*...
|
||||
* - if the invoice is a response to an `invoice_request`:
|
||||
* - MUST reject the invoice if all fields less than type 160 do not exactly match the `invoice_request`.
|
||||
* - if `offer_node_id` is present (invoice_request for an offer):
|
||||
* - MUST reject the invoice if `invoice_node_id` is not equal to `offer_node_id`.
|
||||
* - otherwise (invoice_request without an offer):
|
||||
* - MAY reject the invoice if it cannot confirm that `invoice_node_id` is correct, out-of-band.
|
||||
*
|
||||
* - otherwise: (a invoice presented without being requested, eg. scanned by user):
|
||||
*/
|
||||
|
||||
/* Since the invreq_id hashes all fields < 160, we know it matches */
|
||||
if (arr->size == 0)
|
||||
return fail_inv(cmd, inv, "Unknown invoice_request %s",
|
||||
type_to_string(tmpctx, struct sha256, &inv->invreq_id));
|
||||
|
||||
activetok = json_get_member(buf, arr + 1, "active");
|
||||
if (!activetok) {
|
||||
return fail_internalerr(cmd, inv,
|
||||
"Missing active: %.*s",
|
||||
json_tok_full_len(arr),
|
||||
json_tok_full(buf, arr));
|
||||
}
|
||||
json_to_bool(buf, activetok, &active);
|
||||
if (!active)
|
||||
return fail_inv(cmd, inv, "invoice_request no longer available");
|
||||
|
||||
/* We only save ones without offers to the db! */
|
||||
assert(!inv->inv->offer_node_id);
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - MUST reject the invoice if `signature` is not a valid signature
|
||||
* using `invoice_node_id` as described in [Signature
|
||||
* Calculation](#signature-calculation).
|
||||
*/
|
||||
if (!inv->inv->signature)
|
||||
return fail_inv(cmd, inv, "invoice missing signature");
|
||||
|
||||
merkle_tlv(inv->inv->fields, &merkle);
|
||||
sighash_from_merkle("invoice", "signature", &merkle, &sighash);
|
||||
if (!check_schnorr_sig(&sighash, &inv->inv->invoice_node_id->pubkey, inv->inv->signature))
|
||||
return fail_inv(cmd, inv, "invalid invoice signature");
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - SHOULD confirm authorization if `invoice_amount`.`msat` is not
|
||||
* within the amount range authorized.
|
||||
*/
|
||||
/* Because there's no offer, we had to set invreq_amount */
|
||||
if (*inv->inv->invoice_amount > *inv->inv->invreq_amount)
|
||||
return fail_inv(cmd, inv, "invoice amount is too large");
|
||||
|
||||
/* FIXME: Create a hook for validating the invoice_node_id! */
|
||||
amt = amount_msat(*inv->inv->invoice_amount);
|
||||
plugin_log(cmd->plugin, LOG_INFORM,
|
||||
"Attempting payment of %s for invoice_request %s",
|
||||
type_to_string(tmpctx, struct amount_msat, &amt),
|
||||
type_to_string(tmpctx, struct sha256, &inv->invreq_id));
|
||||
|
||||
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, "localinvreqid", &inv->invreq_id);
|
||||
return send_outreq(cmd->plugin, req);
|
||||
}
|
||||
|
||||
static struct command_result *listinvreqs_error(struct command *cmd,
|
||||
const char *buf,
|
||||
const jsmntok_t *err,
|
||||
struct inv *inv)
|
||||
{
|
||||
return fail_internalerr(cmd, inv,
|
||||
"listinvoicerequests 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;
|
||||
int bad_feature;
|
||||
u64 invexpiry;
|
||||
|
||||
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));
|
||||
}
|
||||
invoice_invreq_id(inv->inv, &inv->invreq_id);
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* A reader of an invoice:
|
||||
* - MUST reject the invoice if `invoice_amount` is not present.
|
||||
* - MUST reject the invoice if `invoice_created_at` is not present.
|
||||
* - MUST reject the invoice if `invoice_payment_hash` is not present.
|
||||
* - MUST reject the invoice if `invoice_node_id` is not present.
|
||||
*/
|
||||
if (!inv->inv->invoice_amount)
|
||||
return fail_inv(cmd, inv, "Missing invoice_amount");
|
||||
if (!inv->inv->invoice_created_at)
|
||||
return fail_inv(cmd, inv, "Missing invoice_created_at");
|
||||
if (!inv->inv->invoice_payment_hash)
|
||||
return fail_inv(cmd, inv, "Missing invoice_payment_hash");
|
||||
if (!inv->inv->invoice_node_id)
|
||||
return fail_inv(cmd, inv, "Missing invoice_node_id");
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* A reader of an invoice:
|
||||
*...
|
||||
* - if `invoice_features` contains unknown _odd_ bits that are non-zero:
|
||||
* - MUST ignore the bit.
|
||||
* - if `invoice_features` contains unknown _even_ bits that are non-zero:
|
||||
* - MUST reject the invoice.
|
||||
*/
|
||||
bad_feature = features_unsupported(plugin_feature_set(cmd->plugin),
|
||||
inv->inv->invoice_features,
|
||||
BOLT12_INVOICE_FEATURE);
|
||||
if (bad_feature != -1) {
|
||||
return fail_inv(cmd, inv,
|
||||
"Unsupported invoice feature %i",
|
||||
bad_feature);
|
||||
}
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* A reader of an invoice:
|
||||
*...
|
||||
* - if `invoice_relative_expiry` is present:
|
||||
* - MUST reject the invoice if the current time since 1970-01-01 UTC is greater than `invoice_created_at` plus `seconds_from_creation`.
|
||||
* - otherwise:
|
||||
* - MUST reject the invoice if the current time since 1970-01-01 UTC is greater than `invoice_created_at` plus 7200.
|
||||
*/
|
||||
if (inv->inv->invoice_relative_expiry)
|
||||
invexpiry = *inv->inv->invoice_created_at + *inv->inv->invoice_relative_expiry;
|
||||
else
|
||||
invexpiry = *inv->inv->invoice_created_at + BOLT12_DEFAULT_REL_EXPIRY;
|
||||
if (time_now().ts.tv_sec > invexpiry)
|
||||
return fail_inv(cmd, inv, "Expired invoice");
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* A reader of an invoice:
|
||||
*...
|
||||
* - MUST reject the invoice if `invoice_paths` is not present or is empty.
|
||||
* - MUST reject the invoice if `invoice_blindedpay` is not present.
|
||||
* - MUST reject the invoice if `invoice_blindedpay` does not contain exactly one `blinded_payinfo` per `invoice_paths`.`blinded_path`.
|
||||
*/
|
||||
if (!inv->inv->invoice_paths)
|
||||
return fail_inv(cmd, inv, "Missing invoice_paths");
|
||||
if (!inv->inv->invoice_blindedpay)
|
||||
return fail_inv(cmd, inv, "Missing invoice_blindedpay");
|
||||
if (tal_count(inv->inv->invoice_blindedpay)
|
||||
!= tal_count(inv->inv->invoice_paths))
|
||||
return fail_inv(cmd, inv,
|
||||
"Mismatch between invoice_blindedpay and invoice_paths");
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* A reader of an invoice:
|
||||
*...
|
||||
* - For each `invoice_blindedpay`.`payinfo`:
|
||||
* - MUST NOT use the corresponding `invoice_paths`.`path` if
|
||||
* `payinfo`.`features` has any unknown even bits set.
|
||||
* - MUST reject the invoice if this leaves no usable paths.
|
||||
*/
|
||||
for (size_t i = 0; i < tal_count(inv->inv->invoice_blindedpay); i++) {
|
||||
bad_feature = features_unsupported(plugin_feature_set(cmd->plugin),
|
||||
inv->inv->invoice_blindedpay[i]->features,
|
||||
/* FIXME: Technically a different feature set? */
|
||||
BOLT12_INVOICE_FEATURE);
|
||||
if (bad_feature == -1)
|
||||
continue;
|
||||
|
||||
tal_arr_remove(&inv->inv->invoice_paths, i);
|
||||
tal_arr_remove(&inv->inv->invoice_blindedpay, i);
|
||||
i--;
|
||||
}
|
||||
if (tal_count(inv->inv->invoice_paths) == 0) {
|
||||
return fail_inv(cmd, inv,
|
||||
"Unsupported feature for all paths (%i)",
|
||||
bad_feature);
|
||||
}
|
||||
|
||||
/* Now find the invoice_request. */
|
||||
req = jsonrpc_request_start(cmd->plugin, cmd, "listinvoicerequests",
|
||||
listinvreqs_done, listinvreqs_error, inv);
|
||||
json_add_sha256(req->js, "invreq_id", &inv->invreq_id);
|
||||
return send_outreq(cmd->plugin, req);
|
||||
}
|
||||
|
11
plugins/offers_inv_hook.h
Normal file
11
plugins/offers_inv_hook.h
Normal file
|
@ -0,0 +1,11 @@
|
|||
#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 */
|
|
@ -940,9 +940,11 @@ static struct command_result *listoffers_done(struct command *cmd,
|
|||
}
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* The writer:
|
||||
* - MUST copy all non-signature fields from the invreq (including
|
||||
* unknown fields).
|
||||
* The writer of an invoice:
|
||||
*...
|
||||
* - if the invoice is in response to an `invoice_request`:
|
||||
* - MUST copy all non-signature fields from the invreq (including
|
||||
* unknown fields).
|
||||
*/
|
||||
ir->inv = invoice_for_invreq(cmd, ir->invreq);
|
||||
assert(ir->inv->invreq_payer_id);
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include <common/json_stream.h>
|
||||
#include <common/overflows.h>
|
||||
#include <plugins/offers_offer.h>
|
||||
#include <sodium/randombytes.h>
|
||||
|
||||
static bool msat_or_any(const char *buffer,
|
||||
const jsmntok_t *tok,
|
||||
|
@ -230,14 +231,14 @@ static struct command_result *check_result(struct command *cmd,
|
|||
&active)) {
|
||||
return command_fail(cmd,
|
||||
LIGHTNINGD,
|
||||
"Bad createoffer status reply %.*s",
|
||||
"Bad createoffer/createinvoicerequest status reply %.*s",
|
||||
json_tok_full_len(result),
|
||||
json_tok_full(buf, result));
|
||||
}
|
||||
if (!active)
|
||||
return command_fail(cmd,
|
||||
OFFER_ALREADY_EXISTS,
|
||||
"Offer already exists, but isn't active");
|
||||
"Already exists, but isn't active");
|
||||
|
||||
/* Otherwise, push through the result. */
|
||||
return forward_result(cmd, buf, result, arg);
|
||||
|
@ -384,3 +385,89 @@ struct command_result *json_offer(struct command *cmd,
|
|||
|
||||
return create_offer(cmd, offinfo);
|
||||
}
|
||||
|
||||
struct command_result *json_invoicerequest(struct command *cmd,
|
||||
const char *buffer,
|
||||
const jsmntok_t *params)
|
||||
{
|
||||
const char *desc, *issuer, *label;
|
||||
struct tlv_invoice_request *invreq;
|
||||
struct out_req *req;
|
||||
struct amount_msat *msat;
|
||||
bool *single_use;
|
||||
|
||||
invreq = tlv_invoice_request_new(cmd);
|
||||
|
||||
if (!param(cmd, buffer, params,
|
||||
p_req("amount", param_msat, &msat),
|
||||
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,
|
||||
&invreq->offer_absolute_expiry),
|
||||
p_opt_def("single_use", param_bool, &single_use, true),
|
||||
NULL))
|
||||
return command_param_failed();
|
||||
|
||||
if (!offers_enabled)
|
||||
return command_fail(cmd, LIGHTNINGD,
|
||||
"experimental-offers not enabled");
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - otherwise (not responding to an offer):
|
||||
* - MUST set (or not set) `offer_metadata`, `offer_description`, `offer_absolute_expiry`, `offer_paths` and `offer_issuer` as it would for an offer.
|
||||
* - MUST set `invreq_payer_id` as it would set `offer_node_id` for an offer.
|
||||
* - MUST NOT include `signature`, `offer_chains`, `offer_amount`, `offer_currency`, `offer_features`, `offer_quantity_max` or `offer_node_id`
|
||||
* - if the chain for the invoice is not solely bitcoin:
|
||||
* - MUST specify `invreq_chain` the offer is valid for.
|
||||
* - MUST set `invreq_amount`.
|
||||
*/
|
||||
invreq->offer_description
|
||||
= tal_dup_arr(invreq, char, desc, strlen(desc), 0);
|
||||
if (issuer) {
|
||||
invreq->offer_issuer
|
||||
= tal_dup_arr(invreq, char, issuer, strlen(issuer), 0);
|
||||
}
|
||||
|
||||
if (!streq(chainparams->network_name, "bitcoin")) {
|
||||
invreq->invreq_chain
|
||||
= tal_dup(invreq, struct bitcoin_blkid,
|
||||
&chainparams->genesis_blockhash);
|
||||
}
|
||||
/* BOLT-offers #12:
|
||||
* - if it sets `invreq_amount`:
|
||||
* - MUST set `msat` in multiples of the minimum lightning-payable unit
|
||||
* (e.g. milli-satoshis for bitcoin) for `invreq_chain` (or for bitcoin, if there is no `invreq_chain`).
|
||||
*/
|
||||
invreq->invreq_amount
|
||||
= tal_dup(invreq, u64, &msat->millisatoshis); /* Raw: wire */
|
||||
|
||||
/* FIXME: enable blinded paths! */
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - MUST set `invreq_metadata` to an unpredictable series of bytes.
|
||||
*/
|
||||
/* BOLT-offers #12:
|
||||
* - otherwise (not responding to an offer):
|
||||
*...
|
||||
* - MUST set `invreq_payer_id` as it would set `offer_node_id` for an offer.
|
||||
*/
|
||||
/* createinvoicerequest sets these! */
|
||||
|
||||
/* BOLT-offers #12:
|
||||
* - if it supports bolt12 invoice request features:
|
||||
* - MUST set `invreq_features`.`features` to the bitmap of features.
|
||||
*/
|
||||
req = jsonrpc_request_start(cmd->plugin, cmd, "createinvoicerequest",
|
||||
check_result, forward_error,
|
||||
invreq);
|
||||
json_add_string(req->js, "bolt12", invrequest_encode(tmpctx, invreq));
|
||||
json_add_bool(req->js, "savetodb", true);
|
||||
/* FIXME: Allow invoicerequests using aliases! */
|
||||
json_add_bool(req->js, "exposeid", true);
|
||||
json_add_bool(req->js, "single_use", *single_use);
|
||||
if (label)
|
||||
json_add_string(req->js, "label", label);
|
||||
return send_outreq(cmd->plugin, req);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ struct command_result *json_offer(struct command *cmd,
|
|||
const char *buffer,
|
||||
const jsmntok_t *params);
|
||||
|
||||
struct command_result *json_offerout(struct command *cmd,
|
||||
const char *buffer,
|
||||
const jsmntok_t *params);
|
||||
struct command_result *json_invoicerequest(struct command *cmd,
|
||||
const char *buffer,
|
||||
const jsmntok_t *params);
|
||||
#endif /* LIGHTNING_PLUGINS_OFFERS_OFFER_H */
|
||||
|
|
|
@ -4379,6 +4379,9 @@ 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('invoicerequest', {'amount': '2msat',
|
||||
'description': 'simple test'})
|
||||
with pytest.raises(RpcError, match='Unknown command'):
|
||||
l1.rpc.call('fetchinvoice', {'offer': 'aaaa'})
|
||||
|
||||
|
@ -4730,7 +4733,7 @@ def test_fetchinvoice(node_factory, bitcoind):
|
|||
'description': 'simple test'})
|
||||
assert offer1['created'] is False
|
||||
l3.rpc.call('disableoffer', {'offer_id': offer1['offer_id']})
|
||||
with pytest.raises(RpcError, match="1000.*Offer already exists, but isn't active"):
|
||||
with pytest.raises(RpcError, match="1000.*Already exists, but isn't active"):
|
||||
l3.rpc.call('offer', {'amount': '2msat',
|
||||
'description': 'simple test'})
|
||||
|
||||
|
@ -4800,6 +4803,16 @@ def test_fetchinvoice_autoconnect(node_factory, bitcoind):
|
|||
l3.rpc.call('fetchinvoice', {'offer': offer['bolt12']})
|
||||
assert l3.rpc.listpeers(l2.info['id'])['peers'] != []
|
||||
|
||||
# Similarly for an invoice_request.
|
||||
l3.rpc.disconnect(l2.info['id'])
|
||||
invreq = l2.rpc.call('invoicerequest', {'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', {'invreq': invreq['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
|
||||
|
@ -4810,7 +4823,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('fetchinvoice', {'offer': offer['bolt12']})
|
||||
l3.rpc.call('sendinvoice', {'invreq': invreq['bolt12'], 'label': 'payme for real!'})
|
||||
# It will have autoconnected, to send invoice (since l1 says it doesn't do onion messages!)
|
||||
assert l3.rpc.listpeers(l2.info['id'])['peers'] != []
|
||||
|
||||
|
@ -4850,6 +4863,71 @@ 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)
|
||||
invreq = l1.rpc.call('invoicerequest', {'amount': '100000sat',
|
||||
'description': 'simple test'})
|
||||
|
||||
# Fetchinvoice will refuse, since it's not an offer.
|
||||
with pytest.raises(RpcError, match='unexpected prefix lnr'):
|
||||
l2.rpc.call('fetchinvoice', {'offer': invreq['bolt12']})
|
||||
|
||||
# Pay will refuse, since it's not an invoice.
|
||||
with pytest.raises(RpcError, match='unexpected prefix lnr'):
|
||||
l2.rpc.call('fetchinvoice', {'offer': invreq['bolt12']})
|
||||
|
||||
# used will be false
|
||||
assert only_one(l1.rpc.call('listinvoicerequests', [invreq['invreq_id']])['invoicerequests'])['used'] is False
|
||||
|
||||
# sendinvoice should work.
|
||||
out = l2.rpc.call('sendinvoice', {'invreq': invreq['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='no longer available|pay attempt failed'):
|
||||
l2.rpc.call('sendinvoice', {'invreq': invreq['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('listinvoicerequests', [invreq['invreq_id']])['invoicerequests'])['used'] is True)
|
||||
|
||||
# Offer with issuer: we must copy issuer into our invoice!
|
||||
invreq = l1.rpc.call('invoicerequest', {'amount': '10000sat',
|
||||
'description': 'simple test',
|
||||
'issuer': "clightning test suite"})
|
||||
|
||||
out = l2.rpc.call('sendinvoice', {'invreq': invreq['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.
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue