From 9681d491df97c675bd7e02ec46072605e6815250 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Sat, 9 Jan 2021 14:55:46 +1030 Subject: [PATCH] offer: allow offers in other currencies if we can convert. This avoids a footgun where they create an offer then we can't create the invoice because they don't have a converter plugin. Signed-off-by: Rusty Russell --- plugins/offers_offer.c | 72 +++++++++++++++++++++++------ tests/plugins/currencyUSDAUD5000.py | 18 ++++++++ tests/test_pay.py | 8 +++- 3 files changed, 82 insertions(+), 16 deletions(-) create mode 100755 tests/plugins/currencyUSDAUD5000.py diff --git a/plugins/offers_offer.c b/plugins/offers_offer.c index 7c227b95b..080968bcf 100644 --- a/plugins/offers_offer.c +++ b/plugins/offers_offer.c @@ -249,22 +249,57 @@ static struct command_result *param_invoice_payment_hash(struct command *cmd, return NULL; } +struct offer_info { + const struct tlv_offer *offer; + const char *label; + bool *single_use; +}; + +static struct command_result *create_offer(struct command *cmd, + struct offer_info *offinfo) +{ + struct out_req *req; + + /* We simply pass this through. */ + req = jsonrpc_request_start(cmd->plugin, cmd, "createoffer", + forward_result, forward_error, + offinfo); + json_add_string(req->js, "bolt12", + offer_encode(tmpctx, offinfo->offer)); + if (offinfo->label) + json_add_string(req->js, "label", offinfo->label); + json_add_bool(req->js, "single_use", *offinfo->single_use); + + return send_outreq(cmd->plugin, req); +} + +static struct command_result *currency_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + struct offer_info *offinfo) +{ + /* Fail in this case, by forwarding warnings. */ + if (!json_get_member(buf, result, "msat")) + return forward_error(cmd, buf, result, offinfo); + + return create_offer(cmd, offinfo); +} + struct command_result *json_offer(struct command *cmd, const char *buffer, const jsmntok_t *params) { - const char *desc, *vendor, *label; + const char *desc, *vendor; struct tlv_offer *offer; - struct out_req *req; - bool *single_use; + struct offer_info *offinfo = tal(cmd, struct offer_info); - offer = tlv_offer_new(cmd); + offinfo->offer = offer = tlv_offer_new(offinfo); if (!param(cmd, buffer, params, p_req("amount", param_amount, offer), p_req("description", param_escaped_string, &desc), p_opt("vendor", param_escaped_string, &vendor), - p_opt("label", param_escaped_string, &label), + p_opt("label", param_escaped_string, &offinfo->label), p_opt("quantity_min", param_u64, &offer->quantity_min), p_opt("quantity_max", param_u64, &offer->quantity_max), p_opt("absolute_expiry", param_u64, &offer->absolute_expiry), @@ -278,7 +313,8 @@ struct command_result *json_offer(struct command *cmd, p_opt("recurrence_limit", param_number, &offer->recurrence_limit), - p_opt_def("single_use", param_bool, &single_use, false), + p_opt_def("single_use", param_bool, + &offinfo->single_use, false), /* FIXME: hints support! */ NULL)) return command_param_failed(); @@ -318,16 +354,22 @@ struct command_result *json_offer(struct command *cmd, offer->node_id = tal_dup(offer, struct pubkey32, &id); - /* We simply pass this through. */ - req = jsonrpc_request_start(cmd->plugin, cmd, "createoffer", - forward_result, forward_error, - offer); - json_add_string(req->js, "bolt12", offer_encode(tmpctx, offer)); - if (label) - json_add_string(req->js, "label", label); - json_add_bool(req->js, "single_use", *single_use); + /* If they specify a different currency, warn if we can't + * convert it! */ + if (offer->currency) { + struct out_req *req; - return send_outreq(cmd->plugin, req); + req = jsonrpc_request_start(cmd->plugin, cmd, "currencyconvert", + currency_done, forward_error, + offinfo); + json_add_u32(req->js, "amount", 1); + json_add_stringn(req->js, "currency", + (const char *)offer->currency, + tal_bytelen(offer->currency)); + return send_outreq(cmd->plugin, req); + } + + return create_offer(cmd, offinfo); } struct command_result *json_offerout(struct command *cmd, diff --git a/tests/plugins/currencyUSDAUD5000.py b/tests/plugins/currencyUSDAUD5000.py new file mode 100755 index 000000000..3a2f99e9a --- /dev/null +++ b/tests/plugins/currencyUSDAUD5000.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +""" +This plugin is used to test the currency command +""" +from pyln.client import Plugin, Millisatoshi + +plugin = Plugin() + + +@plugin.method("currencyconvert") +def currencyconvert(plugin, amount, currency): + """Converts currency using given APIs.""" + if currency in ('USD', 'AUD'): + return {"msat": Millisatoshi(round(amount * 5000))} + raise Exception("No values available for currency {}".format(currency.upper())) + + +plugin.run() diff --git a/tests/test_pay.py b/tests/test_pay.py index 7d39ddf6b..acaf1f5a0 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -3691,7 +3691,8 @@ def test_mpp_overload_payee(node_factory, bitcoind): @unittest.skipIf(not EXPERIMENTAL_FEATURES, "offers are experimental") def test_offer(node_factory, bitcoind): - l1 = node_factory.get_node() + plugin = os.path.join(os.path.dirname(__file__), 'plugins/currencyUSDAUD5000.py') + l1 = node_factory.get_node(options={'plugin': plugin}) bolt12tool = os.path.join(os.path.dirname(__file__), "..", "devtools", "bolt12-cli") # Try different amount strings @@ -3719,6 +3720,11 @@ def test_offer(node_factory, bitcoind): l1.rpc.call('offer', {'amount': '1.1AUD', 'description': 'test for invalid amount'}) + # Make sure it fails on unknown currencies. + with pytest.raises(RpcError, match='No values available for currency EUR'): + l1.rpc.call('offer', {'amount': '1.00EUR', + 'description': 'test for unknown currency'}) + # Test label and description weird_label = 'label \\ " \t \n' weird_desc = 'description \\ " \t \n ナンセンス 1杯'