xpay: option to steal easy commands from pay.

Note: won't work with grpc (or probably other tools), since the output
is different.  But good for testing.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Added: Config: option `xpay-handle-pay` can be used to call xpay when pay is used in many cases (but output is different from pay!)
This commit is contained in:
Rusty Russell
2024-11-17 16:18:06 +10:30
parent 090d605527
commit c715253af7
4 changed files with 223 additions and 4 deletions

View File

@@ -538,6 +538,11 @@ network.
Add a (taproot) fallback address to invoices produced by the `invoice`
command, so they invoices can also be paid onchain.
* **xpay-handle-pay**=*BOOL* [plugin `xpay`, *dynamic*]
Setting this makes `xpay` intercept simply `pay` commands (default `false`). Note that the
response will be different from the normal pay command, however.
### Networking options
Note that for simple setups, the implicit *autolisten* option does the

View File

@@ -919,7 +919,10 @@ static void replace_command(struct rpc_command_hook_payload *p,
fail:
was_pending(command_fail(p->cmd, JSONRPC2_INVALID_REQUEST,
"Bad response to 'rpc_command' hook: %s", bad));
"Bad response to 'rpc_command' hook: %s (%.*s)",
bad,
json_tok_full_len(replacetok),
json_tok_full(buffer, replacetok)));
}
static void rpc_command_hook_final(struct rpc_command_hook_payload *p STEALS)

View File

@@ -33,6 +33,8 @@ struct xpay {
struct pubkey fakenode;
/* We need to know current block height */
u32 blockheight;
/* Do we take over "pay" commands? */
bool take_over_pay;
};
static struct xpay *xpay_of(struct plugin *plugin)
@@ -1591,13 +1593,122 @@ static const struct plugin_notification notifications[] = {
},
};
static struct command_result *handle_rpc_command(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
struct xpay *xpay = xpay_of(cmd->plugin);
const jsmntok_t *rpc_tok, *method_tok, *params_tok, *id_tok,
*bolt11 = NULL, *amount_msat = NULL, *maxfee = NULL,
*partial_msat = NULL, *retry_for = NULL;
struct json_stream *response;
if (!xpay->take_over_pay)
goto dont_redirect;
rpc_tok = json_get_member(buf, params, "rpc_command");
method_tok = json_get_member(buf, rpc_tok, "method");
params_tok = json_get_member(buf, rpc_tok, "params");
id_tok = json_get_member(buf, rpc_tok, "id");
plugin_log(cmd->plugin, LOG_INFORM, "Got command %s",
json_strdup(tmpctx, buf, method_tok));
if (!json_tok_streq(buf, method_tok, "pay"))
goto dont_redirect;
/* Array params? Only handle up to two args (bolt11, msat) */
if (params_tok->type == JSMN_ARRAY) {
if (params_tok->size != 1 && params_tok->size != 2) {
plugin_log(cmd->plugin, LOG_INFORM,
"Not redirecting pay (only handle 1 or 2 args): %.*s",
json_tok_full_len(params),
json_tok_full(buf, params));
goto dont_redirect;
}
bolt11 = params_tok + 1;
if (params_tok->size == 2)
amount_msat = json_next(bolt11);
} else if (params_tok->type == JSMN_OBJECT) {
const jsmntok_t *t;
size_t i;
json_for_each_obj(i, t, params_tok) {
if (json_tok_streq(buf, t, "bolt11"))
bolt11 = t + 1;
else if (json_tok_streq(buf, t, "amount_msat"))
amount_msat = t + 1;
else if (json_tok_streq(buf, t, "retry_for"))
retry_for = t + 1;
else if (json_tok_streq(buf, t, "maxfee"))
maxfee = t + 1;
else if (json_tok_streq(buf, t, "partial_msat"))
partial_msat = t + 1;
else {
plugin_log(cmd->plugin, LOG_INFORM,
"Not redirecting pay (unknown arg %.*s)",
json_tok_full_len(t),
json_tok_full(buf, t));
goto dont_redirect;
}
}
if (!bolt11) {
plugin_log(cmd->plugin, LOG_INFORM,
"Not redirecting pay (missing bolt11 parameter)");
goto dont_redirect;
}
} else {
plugin_log(cmd->plugin, LOG_INFORM,
"Not redirecting pay (unexpected params type)");
goto dont_redirect;
}
plugin_log(cmd->plugin, LOG_INFORM, "Redirecting pay->xpay");
response = jsonrpc_stream_success(cmd);
json_object_start(response, "replace");
json_add_string(response, "jsonrpc", "2.0");
json_add_tok(response, "id", id_tok, buf);
json_add_string(response, "method", "xpay");
json_object_start(response, "params");
json_add_tok(response, "invstring", bolt11, buf);
if (amount_msat)
json_add_tok(response, "amount_msat", amount_msat, buf);
if (retry_for)
json_add_tok(response, "retry_for", retry_for, buf);
if (maxfee)
json_add_tok(response, "maxfee", maxfee, buf);
if (partial_msat)
json_add_tok(response, "partial_msat", partial_msat, buf);
json_object_end(response);
json_object_end(response);
return command_finished(cmd, response);
dont_redirect:
return command_hook_success(cmd);
}
static const struct plugin_hook hooks[] = {
{
"rpc_command",
handle_rpc_command,
},
};
int main(int argc, char *argv[])
{
setup_locale();
struct xpay *xpay;
plugin_main(argv, init, take(tal(NULL, struct xpay)),
setup_locale();
xpay = tal(NULL, struct xpay);
xpay->take_over_pay = false;
plugin_main(argv, init, take(xpay),
PLUGIN_RESTARTABLE, true, NULL,
commands, ARRAY_SIZE(commands),
notifications, ARRAY_SIZE(notifications),
NULL, 0, NULL, 0, NULL, 0, NULL);
hooks, ARRAY_SIZE(hooks),
NULL, 0,
plugin_option_dynamic("xpay-handle-pay", "bool",
"Make xpay take over pay commands it can handle.",
bool_option, bool_jsonfmt, &xpay->take_over_pay),
NULL);
}

View File

@@ -312,3 +312,103 @@ def test_xpay_partial_msat(node_factory, executor):
l1pay.result(TIMEOUT)
l3pay.result(TIMEOUT)
def test_xpay_takeover(node_factory, executor):
l1, l2, l3 = node_factory.line_graph(3, wait_for_announce=True,
opts={'xpay-handle-pay': True,
'experimental-offers': None})
# xpay does NOT look like pay!
l1.rpc.jsonschemas = {}
l2.rpc.jsonschemas = {}
# Simple bolt11/bolt12 payment.
inv = l3.rpc.invoice(100000, "test_xpay_takeover1", "test_xpay_takeover1")['bolt11']
l1.rpc.pay(inv)
l1.daemon.wait_for_log('Redirecting pay->xpay')
# Array version
inv = l3.rpc.invoice(100000, "test_xpay_takeover2", "test_xpay_takeover2")['bolt11']
subprocess.check_output(['cli/lightning-cli',
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'pay',
inv])
l1.daemon.wait_for_log('Redirecting pay->xpay')
offer = l3.rpc.offer(100000, "test_xpay_takeover2")['bolt12']
b12 = l1.rpc.fetchinvoice(offer)['invoice']
l1.rpc.pay(b12)
l1.daemon.wait_for_log('Redirecting pay->xpay')
# BOLT11 with amount.
inv = l3.rpc.invoice('any', "test_xpay_takeover3", "test_xpay_takeover3")['bolt11']
l1.rpc.pay(inv, amount_msat=10000)
l1.daemon.wait_for_log('Redirecting pay->xpay')
# Array version
inv = l3.rpc.invoice('any', "test_xpay_takeover4", "test_xpay_takeover4")['bolt11']
subprocess.check_output(['cli/lightning-cli',
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'pay',
inv, "10000"])
l1.daemon.wait_for_log('Redirecting pay->xpay')
# retry_for, maxfee and partial_msat all work
inv = l3.rpc.invoice('any', "test_xpay_takeover5", "test_xpay_takeover5")['bolt11']
fut1 = executor.submit(l1.rpc.pay, bolt11=inv, amount_msat=2000, retry_for=0, maxfee=100, partial_msat=1000)
l1.daemon.wait_for_log('Redirecting pay->xpay')
fut2 = executor.submit(l2.rpc.pay, bolt11=inv, amount_msat=2000, retry_for=0, maxfee=0, partial_msat=1000)
l2.daemon.wait_for_log('Redirecting pay->xpay')
fut1.result(TIMEOUT)
fut2.result(TIMEOUT)
# Three-array-arg replacements don't work.
inv = l3.rpc.invoice('any', "test_xpay_takeover6", "test_xpay_takeover6")['bolt11']
subprocess.check_output(['cli/lightning-cli',
'--network={}'.format(TEST_NETWORK),
'--lightning-dir={}'
.format(l1.daemon.lightning_dir),
'pay',
inv, "10000", 'label'])
l1.daemon.wait_for_log(r'Not redirecting pay \(only handle 1 or 2 args\): ')
# Other args fail.
inv = l3.rpc.invoice('any', "test_xpay_takeover7", "test_xpay_takeover7")
l1.rpc.pay(inv['bolt11'], amount_msat=10000, label='test_xpay_takeover7')
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"label\\"\)')
inv = l3.rpc.invoice('any', "test_xpay_takeover8", "test_xpay_takeover8")
l1.rpc.pay(inv['bolt11'], amount_msat=10000, riskfactor=1)
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"riskfactor\\"\)')
inv = l3.rpc.invoice('any', "test_xpay_takeover9", "test_xpay_takeover9")
l1.rpc.pay(inv['bolt11'], amount_msat=10000, maxfeepercent=1)
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"maxfeepercent\\"\)')
inv = l3.rpc.invoice('any', "test_xpay_takeover10", "test_xpay_takeover10")
l1.rpc.pay(inv['bolt11'], amount_msat=10000, maxdelay=200)
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"maxdelay\\"\)')
inv = l3.rpc.invoice('any', "test_xpay_takeover11", "test_xpay_takeover11")
l1.rpc.pay(inv['bolt11'], amount_msat=10000, exemptfee=1)
l1.daemon.wait_for_log(r'Not redirecting pay \(unknown arg \\"exemptfee\\"\)')
# Test that it's really dynamic.
l1.rpc.setconfig('xpay-handle-pay', False)
# There's no log for this though!
inv = l3.rpc.invoice(100000, "test_xpay_takeover12", "test_xpay_takeover12")['bolt11']
l1.rpc.pay(inv)
assert not l1.daemon.is_in_log('Redirecting pay->xpay',
start=l1.daemon.logsearch_start)
l1.rpc.setconfig('xpay-handle-pay', True)
inv = l3.rpc.invoice(100000, "test_xpay_takeover13", "test_xpay_takeover13")['bolt11']
l1.rpc.pay(inv)
l1.daemon.wait_for_log('Redirecting pay->xpay')