diff --git a/plugins/autoclean.c b/plugins/autoclean.c index cdbbc81cf..b1670f208 100644 --- a/plugins/autoclean.c +++ b/plugins/autoclean.c @@ -8,11 +8,15 @@ #include enum subsystem { + SUCCEEDEDPAYS, + FAILEDPAYS, PAIDINVOICES, EXPIREDINVOICES, #define NUM_SUBSYSTEM (EXPIREDINVOICES + 1) }; static const char *subsystem_str[] = { + "succeededpays", + "failedpays", "paidinvoices", "expiredinvoices", }; @@ -169,11 +173,73 @@ static struct command_result *listinvoices_done(struct command *cmd, return set_next_timer(plugin); } +static struct command_result *listsendpays_done(struct command *cmd, + const char *buf, + const jsmntok_t *result, + char *unused) +{ + const jsmntok_t *t, *pays = json_get_member(buf, result, "payments"); + size_t i; + u64 now = time_now().ts.tv_sec; + + json_for_each_arr(i, t, pays) { + const jsmntok_t *status = json_get_member(buf, t, "status"); + const jsmntok_t *time; + enum subsystem subsys; + u64 paytime; + + if (json_tok_streq(buf, status, "failed")) { + subsys = FAILEDPAYS; + } else if (json_tok_streq(buf, status, "complete")) { + subsys = SUCCEEDEDPAYS; + } else + continue; + + /* Continue if we don't care. */ + if (subsystem_age[subsys] == 0) + continue; + + time = json_get_member(buf, t, "created_at"); + if (!json_to_u64(buf, time, &paytime)) { + plugin_err(plugin, "Bad created_at '%.*s'", + json_tok_full_len(time), + json_tok_full(buf, time)); + } + + if (paytime <= now - subsystem_age[subsys]) { + struct out_req *req; + const jsmntok_t *phash = json_get_member(buf, t, "payment_hash"); + + req = jsonrpc_request_start(plugin, NULL, "delpay", + del_done, del_failed, + int2ptr(subsys)); + json_add_tok(req->js, "payment_hash", phash, buf); + json_add_tok(req->js, "status", status, buf); + send_outreq(plugin, req); + plugin_log(plugin, LOG_DBG, "Cleaning up %.*s", + json_tok_full_len(phash), json_tok_full(buf, phash)); + cleanup_reqs_remaining++; + } + } + + if (cleanup_reqs_remaining) + return command_still_pending(cmd); + return set_next_timer(plugin); +} + static void do_clean(void *unused) { struct out_req *req = NULL; assert(cleanup_reqs_remaining == 0); + if (subsystem_age[SUCCEEDEDPAYS] != 0 + || subsystem_age[FAILEDPAYS] != 0) { + req = jsonrpc_request_start(plugin, NULL, "listsendpays", + listsendpays_done, cmd_failed, + (char *)"listsendpays"); + send_outreq(plugin, req); + } + if (subsystem_age[EXPIREDINVOICES] != 0 || subsystem_age[PAIDINVOICES] != 0) { req = jsonrpc_request_start(plugin, NULL, "listinvoices", diff --git a/tests/test_plugin.py b/tests/test_plugin.py index a9d83a9e4..3c5dc0504 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -2943,6 +2943,7 @@ def test_autoclean(node_factory): l1.rpc.invoice(amount_msat=12300, label='inv2', description='description2', expiry=20) l1.rpc.invoice(amount_msat=12300, label='inv3', description='description3', expiry=20) inv4 = l1.rpc.invoice(amount_msat=12300, label='inv4', description='description4', expiry=2000) + inv5 = l1.rpc.invoice(amount_msat=12300, label='inv5', description='description5', expiry=2000) l1.rpc.autoclean(subsystem='expiredinvoices', age=2) assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is True assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['age'] == 2 @@ -2959,8 +2960,9 @@ def test_autoclean(node_factory): wait_for(lambda: l1.rpc.listinvoices('inv1')['invoices'] == []) assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 1 - # Keeps settings across restarts. + # Keeps settings across restarts l1.restart() + assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['enabled'] is True assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['age'] == 2 assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 1 @@ -2985,13 +2987,18 @@ def test_autoclean(node_factory): # Now enable: they will get autocleaned l1.rpc.autoclean(subsystem='expiredinvoices', age=2) - wait_for(lambda: len(l1.rpc.listinvoices()['invoices']) == 1) + wait_for(lambda: len(l1.rpc.listinvoices()['invoices']) == 2) assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 3 # Reconnect, l0 pays invoice, we test paid expiry. l1.rpc.connect(l0.info['id'], 'localhost', l0.port) l0.rpc.pay(inv4['bolt11']) + # We manually delete inv5 so we can have l0 fail a payment. + l1.rpc.delinvoice('inv5', 'unpaid') + with pytest.raises(RpcError, match='WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS'): + l0.rpc.pay(inv5['bolt11']) + assert l1.rpc.autoclean_status()['autoclean']['paidinvoices']['enabled'] is False assert l1.rpc.autoclean_status()['autoclean']['paidinvoices']['cleaned'] == 0 l1.rpc.autoclean(subsystem='paidinvoices', age=1) @@ -3001,6 +3008,18 @@ def test_autoclean(node_factory): assert l1.rpc.autoclean_status()['autoclean']['expiredinvoices']['cleaned'] == 3 assert l1.rpc.autoclean_status()['autoclean']['paidinvoices']['cleaned'] == 1 + assert only_one(l0.rpc.listpays(inv5['bolt11'])['pays'])['status'] == 'failed' + assert only_one(l0.rpc.listpays(inv4['bolt11'])['pays'])['status'] == 'complete' + l0.rpc.autoclean(subsystem='failedpays', age=2) + + wait_for(lambda: l0.rpc.listpays(inv5['bolt11'])['pays'] == []) + assert l0.rpc.autoclean_status()['autoclean']['failedpays']['cleaned'] == 1 + assert l0.rpc.autoclean_status()['autoclean']['succeededpays']['cleaned'] == 0 + + l0.rpc.autoclean(subsystem='succeededpays', age=2) + wait_for(lambda: l0.rpc.listpays(inv4['bolt11'])['pays'] == []) + assert l0.rpc.listsendpays() == {'payments': []} + def test_block_added_notifications(node_factory, bitcoind): """Test if a plugin gets notifications when a new block is found"""