mirror of
https://github.com/aljazceru/lightning.git
synced 2025-12-23 17:14:22 +01:00
plugins/fetchinvoice: handle sendinvoice timeout, error or payment.
If they pay the invoice, they don't bother replying; that's just for errors. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
This commit is contained in:
@@ -83,6 +83,7 @@ static const errcode_t OFFER_ALREADY_DISABLED = 1001;
|
|||||||
static const errcode_t OFFER_EXPIRED = 1002;
|
static const errcode_t OFFER_EXPIRED = 1002;
|
||||||
static const errcode_t OFFER_ROUTE_NOT_FOUND = 1003;
|
static const errcode_t OFFER_ROUTE_NOT_FOUND = 1003;
|
||||||
static const errcode_t OFFER_BAD_INVREQ_REPLY = 1004;
|
static const errcode_t OFFER_BAD_INVREQ_REPLY = 1004;
|
||||||
|
static const errcode_t OFFER_TIMEOUT = 1005;
|
||||||
|
|
||||||
/* Errors from wait* commands */
|
/* Errors from wait* commands */
|
||||||
static const errcode_t WAIT_TIMEOUT = 2000;
|
static const errcode_t WAIT_TIMEOUT = 2000;
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ struct sent {
|
|||||||
struct tlv_invoice *inv;
|
struct tlv_invoice *inv;
|
||||||
struct preimage inv_preimage;
|
struct preimage inv_preimage;
|
||||||
struct json_escape *inv_label;
|
struct json_escape *inv_label;
|
||||||
|
/* How long to wait for response before giving up. */
|
||||||
|
u32 inv_wait_timeout;
|
||||||
};
|
};
|
||||||
|
|
||||||
static struct sent *find_sent(const struct pubkey *blinding)
|
static struct sent *find_sent(const struct pubkey *blinding)
|
||||||
@@ -371,15 +373,6 @@ badinv:
|
|||||||
return command_hook_success(cmd);
|
return command_hook_success(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct command_result *handle_inv_response(struct command *cmd,
|
|
||||||
struct sent *sent,
|
|
||||||
const char *buf,
|
|
||||||
const jsmntok_t *om)
|
|
||||||
{
|
|
||||||
/* FIXME: Report error. */
|
|
||||||
return command_hook_success(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
static struct command_result *recv_onion_message(struct command *cmd,
|
static struct command_result *recv_onion_message(struct command *cmd,
|
||||||
const char *buf,
|
const char *buf,
|
||||||
const jsmntok_t *params)
|
const jsmntok_t *params)
|
||||||
@@ -413,10 +406,8 @@ static struct command_result *recv_onion_message(struct command *cmd,
|
|||||||
|
|
||||||
if (sent->invreq)
|
if (sent->invreq)
|
||||||
return handle_invreq_response(cmd, sent, buf, om);
|
return handle_invreq_response(cmd, sent, buf, om);
|
||||||
else {
|
|
||||||
assert(sent->inv);
|
return command_hook_success(cmd);
|
||||||
return handle_inv_response(cmd, sent, buf, om);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void destroy_sent(struct sent *sent)
|
static void destroy_sent(struct sent *sent)
|
||||||
@@ -620,6 +611,31 @@ static struct command_result *send_message(struct command *cmd,
|
|||||||
return send_outreq(cmd->plugin, req);
|
return send_outreq(cmd->plugin, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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_addstr(details, "invstring", invoice_encode(tmpctx, sent->inv));
|
||||||
|
/* This will free sent! */
|
||||||
|
discard_result(command_done_err(sent->cmd, OFFER_TIMEOUT,
|
||||||
|
"Timeout waiting for response"
|
||||||
|
" (but use waitinvoice if invoice_timeout"
|
||||||
|
" was greater)",
|
||||||
|
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->inv_wait_timeout),
|
||||||
|
timeout_sent_inv, sent));
|
||||||
|
return sendonionmsg_done(cmd, buf, result, sent);
|
||||||
|
}
|
||||||
|
|
||||||
static struct command_result *invreq_done(struct command *cmd,
|
static struct command_result *invreq_done(struct command *cmd,
|
||||||
const char *buf,
|
const char *buf,
|
||||||
const jsmntok_t *result,
|
const jsmntok_t *result,
|
||||||
@@ -851,6 +867,54 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
|
|||||||
return send_outreq(cmd->plugin, req);
|
return send_outreq(cmd->plugin, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FIXME: Using a hook here is not ideal: technically it doesn't mean
|
||||||
|
* it's actually hit the db! But using waitinvoice is also suboptimal
|
||||||
|
* because we don't have libplugin infra to cancel a pending req (and I
|
||||||
|
* want to rewrite our wait* API anyway) */
|
||||||
|
static struct command_result *invoice_payment(struct command *cmd,
|
||||||
|
const char *buf,
|
||||||
|
const jsmntok_t *params)
|
||||||
|
{
|
||||||
|
struct sent *i;
|
||||||
|
const jsmntok_t *ptok, *preimagetok, *msattok;
|
||||||
|
struct preimage preimage;
|
||||||
|
struct amount_msat msat;
|
||||||
|
|
||||||
|
ptok = json_get_member(buf, params, "payment");
|
||||||
|
preimagetok = json_get_member(buf, ptok, "preimage");
|
||||||
|
msattok = json_get_member(buf, ptok, "msat");
|
||||||
|
if (!preimagetok || !msattok)
|
||||||
|
plugin_err(cmd->plugin,
|
||||||
|
"Invalid invoice_payment %.*s",
|
||||||
|
json_tok_full_len(params),
|
||||||
|
json_tok_full(buf, params));
|
||||||
|
|
||||||
|
hex_decode(buf + preimagetok->start,
|
||||||
|
preimagetok->end - preimagetok->start,
|
||||||
|
&preimage, sizeof(preimage));
|
||||||
|
json_to_msat(buf, msattok, &msat);
|
||||||
|
|
||||||
|
list_for_each(&sent_list, i, list) {
|
||||||
|
struct json_stream *out;
|
||||||
|
|
||||||
|
if (!i->inv)
|
||||||
|
continue;
|
||||||
|
if (!preimage_eq(&preimage, &i->inv_preimage))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
/* It was paid! Success. */
|
||||||
|
/* FIXME: Return as per waitinvoice */
|
||||||
|
out = jsonrpc_stream_success(i->cmd);
|
||||||
|
json_add_string(out, "invstring", invoice_encode(tmpctx, i->inv));
|
||||||
|
json_add_string(out, "msat",
|
||||||
|
type_to_string(tmpctx, struct amount_msat,
|
||||||
|
&msat));
|
||||||
|
discard_result(command_finished(i->cmd, out));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return command_hook_success(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
static struct command_result *createinvoice_done(struct command *cmd,
|
static struct command_result *createinvoice_done(struct command *cmd,
|
||||||
const char *buf,
|
const char *buf,
|
||||||
const jsmntok_t *result,
|
const jsmntok_t *result,
|
||||||
@@ -880,7 +944,7 @@ static struct command_result *createinvoice_done(struct command *cmd,
|
|||||||
|
|
||||||
rawinv = tal_arr(tmpctx, u8, 0);
|
rawinv = tal_arr(tmpctx, u8, 0);
|
||||||
towire_invoice(&rawinv, sent->inv);
|
towire_invoice(&rawinv, sent->inv);
|
||||||
return send_message(cmd, sent, "invoice", rawinv, sendonionmsg_done);
|
return send_message(cmd, sent, "invoice", rawinv, prepare_inv_timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct command_result *sign_invoice(struct command *cmd,
|
static struct command_result *sign_invoice(struct command *cmd,
|
||||||
@@ -1007,6 +1071,7 @@ static struct command_result *json_sendinvoice(struct command *cmd,
|
|||||||
{
|
{
|
||||||
struct amount_msat *msat;
|
struct amount_msat *msat;
|
||||||
struct out_req *req;
|
struct out_req *req;
|
||||||
|
u32 *timeout, *invoice_timeout;
|
||||||
struct sent *sent = tal(cmd, struct sent);
|
struct sent *sent = tal(cmd, struct sent);
|
||||||
|
|
||||||
sent->inv = tlv_invoice_new(cmd);
|
sent->inv = tlv_invoice_new(cmd);
|
||||||
@@ -1018,10 +1083,15 @@ static struct command_result *json_sendinvoice(struct command *cmd,
|
|||||||
p_req("offer", param_offer, &sent->offer),
|
p_req("offer", param_offer, &sent->offer),
|
||||||
p_req("label", param_label, &sent->inv_label),
|
p_req("label", param_label, &sent->inv_label),
|
||||||
p_opt("msatoshi", param_msat, &msat),
|
p_opt("msatoshi", param_msat, &msat),
|
||||||
|
p_opt_def("timeout", param_number, &timeout, 90),
|
||||||
|
p_opt("invoice_timeout", param_number, &invoice_timeout),
|
||||||
p_opt("quantity", param_u64, &sent->inv->quantity),
|
p_opt("quantity", param_u64, &sent->inv->quantity),
|
||||||
NULL))
|
NULL))
|
||||||
return command_param_failed();
|
return command_param_failed();
|
||||||
|
|
||||||
|
/* This is how long we'll wait for a reply for. */
|
||||||
|
sent->inv_wait_timeout = *timeout;
|
||||||
|
|
||||||
/* Check they are really trying to send us money. */
|
/* Check they are really trying to send us money. */
|
||||||
if (!sent->offer->send_invoice)
|
if (!sent->offer->send_invoice)
|
||||||
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||||
@@ -1105,6 +1175,34 @@ static struct command_result *json_sendinvoice(struct command *cmd,
|
|||||||
"quantity parameter unnecessary");
|
"quantity parameter unnecessary");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* BOLT-offers #12:
|
||||||
|
* - MUST set `timestamp` to the number of seconds since Midnight 1
|
||||||
|
* January 1970, UTC.
|
||||||
|
*/
|
||||||
|
sent->inv->timestamp = tal(sent->inv, u64);
|
||||||
|
*sent->inv->timestamp = time_now().ts.tv_sec;
|
||||||
|
|
||||||
|
/* If they don't specify an invoice_timeout, make it the same as we're
|
||||||
|
* prepare to wait. */
|
||||||
|
if (!invoice_timeout)
|
||||||
|
invoice_timeout = timeout;
|
||||||
|
else if (*invoice_timeout < *timeout)
|
||||||
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
||||||
|
"invoice_timeout %u must be >= timeout %u",
|
||||||
|
*invoice_timeout, *timeout);
|
||||||
|
|
||||||
|
/* BOLT-offers #12:
|
||||||
|
* - if the expiry for accepting payment is not 7200 seconds after
|
||||||
|
* `timestamp`:
|
||||||
|
* - MUST set `relative_expiry` `seconds_from_timestamp` to the number
|
||||||
|
* of seconds after `timestamp` that payment of this invoice should
|
||||||
|
* not be attempted.
|
||||||
|
*/
|
||||||
|
if (*invoice_timeout != 7200) {
|
||||||
|
sent->inv->relative_expiry = tal(sent->inv, u32);
|
||||||
|
*sent->inv->relative_expiry = *invoice_timeout;
|
||||||
|
}
|
||||||
|
|
||||||
/* BOLT-offers #12:
|
/* BOLT-offers #12:
|
||||||
* - MUST set `payer_key` to the `node_id` of the offer.
|
* - MUST set `payer_key` to the `node_id` of the offer.
|
||||||
*/
|
*/
|
||||||
@@ -1191,6 +1289,10 @@ static const struct plugin_hook hooks[] = {
|
|||||||
"onion_message_blinded",
|
"onion_message_blinded",
|
||||||
recv_onion_message
|
recv_onion_message
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"invoice_payment",
|
||||||
|
invoice_payment,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
|
|||||||
@@ -3967,3 +3967,29 @@ def test_sendinvoice(node_factory, bitcoind):
|
|||||||
# Fetchinvoice will refuse, since you're supposed to send an invoice.
|
# Fetchinvoice will refuse, since you're supposed to send an invoice.
|
||||||
with pytest.raises(RpcError, match='Offer wants an invoice, not invoice_request'):
|
with pytest.raises(RpcError, match='Offer wants an invoice, not invoice_request'):
|
||||||
l2.rpc.call('fetchinvoice', {'offer': offer})
|
l2.rpc.call('fetchinvoice', {'offer': offer})
|
||||||
|
|
||||||
|
# sendinvoice should work.
|
||||||
|
out = l2.rpc.call('sendinvoice', {'offer': offer,
|
||||||
|
'label': 'test sendinvoice 1'})
|
||||||
|
print(out)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
'label': 'test sendinvoice 2'})
|
||||||
|
|
||||||
|
# Now try a refund.
|
||||||
|
offer = l2.rpc.call('offer', {'amount': '100msat',
|
||||||
|
'description': 'simple test'})['bolt12']
|
||||||
|
|
||||||
|
inv = l1.rpc.call('fetchinvoice', {'offer': offer})
|
||||||
|
l1.rpc.pay(inv['invoice'])
|
||||||
|
|
||||||
|
refund = l2.rpc.call('offer', {'amount': '100msat',
|
||||||
|
'description': 'refund test',
|
||||||
|
'refund_for': inv['invoice']})['bolt12']
|
||||||
|
|
||||||
|
l1.rpc.call('sendinvoice', {'offer': refund,
|
||||||
|
'label': 'test sendinvoice refund'})
|
||||||
|
|||||||
Reference in New Issue
Block a user