Browse Source

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>
ppa
Rusty Russell 4 years ago
parent
commit
075c25fc08
  1. 1
      common/jsonrpc_errors.h
  2. 130
      plugins/fetchinvoice.c
  3. 26
      tests/test_pay.py

1
common/jsonrpc_errors.h

@ -83,6 +83,7 @@ static const errcode_t OFFER_ALREADY_DISABLED = 1001;
static const errcode_t OFFER_EXPIRED = 1002;
static const errcode_t OFFER_ROUTE_NOT_FOUND = 1003;
static const errcode_t OFFER_BAD_INVREQ_REPLY = 1004;
static const errcode_t OFFER_TIMEOUT = 1005;
/* Errors from wait* commands */
static const errcode_t WAIT_TIMEOUT = 2000;

130
plugins/fetchinvoice.c

@ -44,6 +44,8 @@ struct sent {
struct tlv_invoice *inv;
struct preimage inv_preimage;
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)
@ -371,15 +373,6 @@ badinv:
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,
const char *buf,
const jsmntok_t *params)
@ -413,10 +406,8 @@ static struct command_result *recv_onion_message(struct command *cmd,
if (sent->invreq)
return handle_invreq_response(cmd, sent, buf, om);
else {
assert(sent->inv);
return handle_inv_response(cmd, sent, buf, om);
}
return command_hook_success(cmd);
}
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);
}
/* 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,
const char *buf,
const jsmntok_t *result,
@ -851,6 +867,54 @@ static struct command_result *json_fetchinvoice(struct command *cmd,
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,
const char *buf,
const jsmntok_t *result,
@ -880,7 +944,7 @@ static struct command_result *createinvoice_done(struct command *cmd,
rawinv = tal_arr(tmpctx, u8, 0);
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,
@ -1007,6 +1071,7 @@ static struct command_result *json_sendinvoice(struct command *cmd,
{
struct amount_msat *msat;
struct out_req *req;
u32 *timeout, *invoice_timeout;
struct sent *sent = tal(cmd, struct sent);
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("label", param_label, &sent->inv_label),
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),
NULL))
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. */
if (!sent->offer->send_invoice)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
@ -1105,6 +1175,34 @@ static struct command_result *json_sendinvoice(struct command *cmd,
"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:
* - MUST set `payer_key` to the `node_id` of the offer.
*/
@ -1191,6 +1289,10 @@ static const struct plugin_hook hooks[] = {
"onion_message_blinded",
recv_onion_message
},
{
"invoice_payment",
invoice_payment,
},
};
int main(int argc, char *argv[])

26
tests/test_pay.py

@ -3967,3 +3967,29 @@ def test_sendinvoice(node_factory, bitcoind):
# 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})
# 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'})

Loading…
Cancel
Save