From ad1e1bd528332c65d66526adb25be38c10c098db Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Tue, 15 Jan 2019 14:44:27 +1030 Subject: [PATCH] plugins: minimal 'pay' plugin. I wrote this sync first, then rewrote async, then developed libplugin. But committing all that just wastes reviewer time, so I present it as if it was always asnc and using the library helper. Currently the command it registers is 'pay2', but when it's complete we'll remove the internal 'pay' and rename it. This does a single 'getroute/sendpay' call. No retries, no options. Shockingly, this by itself is almost sufficient to pass our current test suite with `pay`->`pay2`. Signed-off-by: Rusty Russell --- plugins/.gitignore | 1 + plugins/Makefile | 16 +++++- plugins/pay.c | 125 +++++++++++++++++++++++++++++++++++++++++++ tests/test_plugin.py | 8 +++ 4 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 plugins/.gitignore create mode 100644 plugins/pay.c diff --git a/plugins/.gitignore b/plugins/.gitignore new file mode 100644 index 000000000..65847d8a1 --- /dev/null +++ b/plugins/.gitignore @@ -0,0 +1 @@ +pay diff --git a/plugins/Makefile b/plugins/Makefile index 7ac09f187..2199a5688 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -1,3 +1,6 @@ +PLUGIN_PAY_SRC := plugins/pay.c +PLUGIN_PAY_OBJS := $(PLUGIN_PAY_SRC:.c=.o) + PLUGIN_LIB_SRC := plugins/libplugin.c PLUGIN_LIB_HEADER := plugins/libplugin.h PLUGIN_LIB_OBJS := $(PLUGIN_LIB_SRC:.c=.o) @@ -28,10 +31,19 @@ PLUGIN_COMMON_OBJS := \ wire/fromwire.o \ wire/towire.o +plugins/pay: $(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) $(CCAN_OBJS) + +$(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS): $(PLUGIN_LIB_HEADER) + # Make sure these depend on everything. -ALL_OBJS += $(PLUGIN_LIB_OBJS) +ALL_PROGRAMS += plugins/pay +ALL_OBJS += $(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS) + +check-source: $(PLUGIN_PAY_SRC:%=check-src-include-order/%) +check-source-bolt: $(PLUGIN_PAY_SRC:%=bolt-check/%) +check-whitespace: $(PLUGIN_PAY_SRC:%=check-whitespace/%) clean: plugin-clean plugin-clean: - $(RM) $(PLUGIN_LIB_OBJS) + $(RM) $(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS) diff --git a/plugins/pay.c b/plugins/pay.c new file mode 100644 index 000000000..28691c9a8 --- /dev/null +++ b/plugins/pay.c @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include +#include + +struct pay_command { + /* Payment hash, as text. */ + const char *payment_hash; + + /* Description, if any. */ + const char *desc; +}; + +static struct command_result *sendpay_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct pay_command *pc) +{ + return send_outreq(cmd, "waitsendpay", + forward_result, forward_error, pc, + "'payment_hash': '%s', 'timeout': 60", + pc->payment_hash); +} + +static struct command_result *getroute_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct pay_command *pc) +{ + const jsmntok_t *t = json_get_member(buf, result, "route"); + char *json_desc; + if (!t) + plugin_err("getroute gave no 'route'? '%.*s'", + result->end - result->start, buf); + + if (pc->desc) + json_desc = tal_fmt(pc, ", 'description': '%s'", pc->desc); + else + json_desc = ""; + + return send_outreq(cmd, "sendpay", sendpay_done, forward_error, pc, + "'route': %.*s, 'payment_hash': '%s'%s", + t->end - t->start, buf + t->start, + pc->payment_hash, + json_desc); +} + +static struct command_result *handle_pay(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + u64 *msatoshi; + struct bolt11 *b11; + const char *b11str; + char *fail; + double *riskfactor; + struct pay_command *pc = tal(cmd, struct pay_command); + + /* FIXME! */ + double *maxfeepercent; + unsigned int *retryfor; + unsigned int *maxdelay; + unsigned int *exemptfee; + + setup_locale(); + + if (!param(cmd, buf, params, + p_req("bolt11", param_string, &b11str), + p_opt("msatoshi", param_u64, &msatoshi), + p_opt("description", param_string, &pc->desc), + p_opt_def("riskfactor", param_double, &riskfactor, 1.0), + p_opt_def("maxfeepercent", param_percent, &maxfeepercent, 0.5), + p_opt_def("retry_for", param_number, &retryfor, 60), + p_opt_def("maxdelay", param_number, &maxdelay, + /* FIXME! */ + 14 * 24 * 6), + p_opt_def("exemptfee", param_number, &exemptfee, 5000), + NULL)) + return NULL; + + b11 = bolt11_decode(cmd, b11str, pc->desc, &fail); + if (!b11) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Invalid bolt11: %s", fail); + } + + if (b11->msatoshi) { + if (msatoshi) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "msatoshi parameter unnecessary"); + } + } else { + if (!msatoshi) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "msatoshi parameter required"); + } + b11->msatoshi = tal_steal(b11, msatoshi); + } + + pc->payment_hash = type_to_string(pc, struct sha256, + &b11->payment_hash); + + /* OK, ask for route to destination */ + return send_outreq(cmd, "getroute", getroute_done, forward_error, pc, + "'id': '%s'," + "'msatoshi': %"PRIu64"," + "'riskfactor': %f", + type_to_string(tmpctx, struct pubkey, &b11->receiver_id), + *b11->msatoshi, *riskfactor); +} + +static const struct plugin_command commands[] = { { + "pay2", + "Send payment specified by {bolt11} with {msatoshi}", + "Try to send a payment, retrying {retry_for} seconds before giving up", + handle_pay + } +}; + +int main(int argc, char *argv[]) +{ + plugin_main(argv, NULL, commands, ARRAY_SIZE(commands)); +} diff --git a/tests/test_plugin.py b/tests/test_plugin.py index d76a563ab..aacf77606 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -108,3 +108,11 @@ def test_failing_plugins(): '--plugin={}'.format(p), '--help', ]) + + +def test_pay_plugin(node_factory): + l1, l2 = node_factory.line_graph(2) + inv = l2.rpc.invoice(123000, 'label', 'description', 3700) + + res = l1.rpc.pay2(bolt11=inv['bolt11']) + assert res['status'] == 'complete'