diff --git a/plugins/pay.c b/plugins/pay.c index 1cdd33937..b53dc9b71 100644 --- a/plugins/pay.c +++ b/plugins/pay.c @@ -1,5 +1,7 @@ #include #include +#include +#include #include #include #include @@ -1265,13 +1267,13 @@ static struct command_result *json_paystatus(struct command *cmd, return command_success(cmd, ret); } -static bool attempt_ongoing(const char *buf, const jsmntok_t *b11) +static bool attempt_ongoing(const char *b11) { struct pay_status *ps; struct pay_attempt *attempt; list_for_each(&pay_status, ps, list) { - if (!json_tok_streq(buf, b11, ps->bolt11)) + if (!streq(b11, ps->bolt11)) continue; attempt = &ps->attempts[tal_count(ps->attempts)-1]; return attempt->result == NULL && attempt->failure == NULL; @@ -1279,6 +1281,79 @@ static bool attempt_ongoing(const char *buf, const jsmntok_t *b11) return false; } +/* We consolidate multi-part payments into a single entry. */ +struct pay_mpp { + /* This is the bolt11 string, and lookup key */ + const char *b11; + /* Status of combined payment */ + const char *status; + /* Optional label (of first one!) */ + const jsmntok_t *label; + /* Optional preimage (iff status is successful) */ + const jsmntok_t *preimage; + /* Only counts "complete" or "pending" payments. */ + size_t num_nonfailed_parts; + /* Total amount sent ("complete" or "pending" only). */ + struct amount_msat amount_sent; +}; + +static const char *pay_mpp_key(const struct pay_mpp *pm) +{ + return pm->b11; +} + +static size_t b11str_hash(const char *b11) +{ + return siphash24(siphash_seed(), b11, strlen(b11)); +} + +static bool pay_mpp_eq(const struct pay_mpp *pm, const char *b11) +{ + return streq(pm->b11, b11); +} + +HTABLE_DEFINE_TYPE(struct pay_mpp, pay_mpp_key, b11str_hash, pay_mpp_eq, + pay_map); + +static void add_amount_sent(const char *b11, + struct amount_msat *total, + const char *buf, + const jsmntok_t *t) +{ + struct amount_msat sent; + json_to_msat(buf, json_get_member(buf, t, "amount_sent_msat"), &sent); + if (!amount_msat_add(total, *total, sent)) + plugin_log(LOG_BROKEN, + "Cannot add amount_sent_msat for %s: %s + %s", + b11, + type_to_string(tmpctx, struct amount_msat, total), + type_to_string(tmpctx, struct amount_msat, &sent)); +} + +static void add_new_entry(struct json_out *ret, + const char *buf, + const struct pay_mpp *pm) +{ + json_out_start(ret, NULL, '{'); + json_out_addstr(ret, "bolt11", pm->b11); + json_out_addstr(ret, "status", pm->status); + if (pm->label) + json_out_add_raw_len(ret, "label", + json_tok_full(buf, pm->label), + json_tok_full_len(pm->label)); + if (pm->preimage) + json_out_add_raw_len(ret, "preimage", + json_tok_full(buf, pm->preimage), + json_tok_full_len(pm->preimage)); + json_out_addstr(ret, "amount_sent_msat", + fmt_amount_msat(tmpctx, &pm->amount_sent)); + + if (pm->num_nonfailed_parts > 1) + json_out_add_u64(ret, "number_of_parts", + pm->num_nonfailed_parts); + json_out_end(ret, '}'); +} + static struct command_result *listsendpays_done(struct command *cmd, const char *buf, const jsmntok_t *result, @@ -1287,6 +1362,11 @@ static struct command_result *listsendpays_done(struct command *cmd, size_t i; const jsmntok_t *t, *arr; struct json_out *ret; + struct pay_map pay_map; + struct pay_map_iter it; + struct pay_mpp *pm; + + pay_map_init(&pay_map); arr = json_get_member(buf, result, "payments"); if (!arr || arr->type != JSMN_ARRAY) @@ -1297,31 +1377,60 @@ static struct command_result *listsendpays_done(struct command *cmd, json_out_start(ret, NULL, '{'); json_out_start(ret, "pays", '['); json_for_each_arr(i, t, arr) { - const jsmntok_t *status, *b11; + const jsmntok_t *status, *b11tok; + const char *b11; - json_out_start(ret, NULL, '{'); - b11 = copy_member(ret, buf, t, "bolt11"); + b11tok = json_get_member(buf, t, "bolt11"); /* Old (or manual) payments didn't have bolt11 field */ - if (!b11) + if (!b11tok) continue; - /* listsendpays might say it failed, but we're still retrying */ + b11 = json_strdup(cmd, buf, b11tok); + + pm = pay_map_get(&pay_map, b11); + if (!pm) { + pm = tal(cmd, struct pay_mpp); + pm->b11 = tal_steal(pm, b11); + pm->label = json_get_member(buf, t, "label"); + pm->preimage = NULL; + pm->amount_sent = AMOUNT_MSAT(0); + pm->num_nonfailed_parts = 0; + pm->status = NULL; + pay_map_add(&pay_map, pm); + } + status = json_get_member(buf, t, "status"); - if (status) { - if (json_tok_streq(buf, status, "failed") - && attempt_ongoing(buf, b11)) { - json_out_addstr(ret, "status", "pending"); - } else { - copy_member(ret, buf, t, "status"); - if (json_tok_streq(buf, status, "complete")) - copy_member(ret, buf, t, - "payment_preimage"); - } + if (json_tok_streq(buf, status, "complete")) { + add_amount_sent(pm->b11, &pm->amount_sent, buf, t); + pm->num_nonfailed_parts++; + pm->status = "complete"; + pm->preimage + = json_get_member(buf, t, "payment_preimage"); + } else if (json_tok_streq(buf, status, "pending")) { + add_amount_sent(pm->b11, &pm->amount_sent, buf, t); + pm->num_nonfailed_parts++; + /* Failed -> pending; don't downgrade success. */ + if (!pm->status || !streq(pm->status, "complete")) + pm->status = "pending"; + } else { + if (attempt_ongoing(pm->b11)) { + /* Failed -> pending; don't downgrade success. */ + if (!pm->status + || !streq(pm->status, "complete")) + pm->status = "pending"; + } else if (!pm->status) + /* Only failed if they all failed */ + pm->status = "failed"; } - copy_member(ret, buf, t, "label"); - copy_member(ret, buf, t, "amount_sent_msat"); - json_out_end(ret, '}'); } + + /* Now we've collapsed them, provide summary (free mem as we go). */ + while ((pm = pay_map_first(&pay_map, &it)) != NULL) { + add_new_entry(ret, buf, pm); + pay_map_del(&pay_map, pm); + } + pay_map_clear(&pay_map); + json_out_end(ret, ']'); json_out_end(ret, '}'); return command_success(cmd, ret); diff --git a/tests/test_pay.py b/tests/test_pay.py index 3c5119f1a..f2626f30a 100644 --- a/tests/test_pay.py +++ b/tests/test_pay.py @@ -2641,3 +2641,9 @@ def test_partial_payment(node_factory, bitcoind, executor): assert "'forward_amount': '500msat'" in line assert "'total_msat': '1000msat'" in line assert "'payment_secret': '{}'".format(paysecret) in line + + pay = only_one(l1.rpc.listpays()['pays']) + assert pay['bolt11'] == inv['bolt11'] + assert pay['status'] == 'complete' + assert pay['number_of_parts'] == 2 + assert pay['amount_sent_msat'] == Millisatoshi(1002)