xpay: add pay_part_start and pay_part_end notifications.
Requested-by: Michael at Boltz Signed-off-by: Rusty Russell <rusty@rustcorp.com.au> Changelog-Added: Plugins: `xpay` now publishes `pay_part_start` and `pay_part_end` notifications on every payment send attempt.
This commit is contained in:
@@ -567,3 +567,89 @@ Where:
|
||||
- `plugin_name`: The short name of the plugin.
|
||||
- `plugin_path`: The full file path to the plugin executable.
|
||||
- `methods`: An array of RPC method names that the plugin registered.
|
||||
|
||||
|
||||
### `pay_part_start` (v25.09 onward)
|
||||
|
||||
Emitted by `xpay` when part of a payment begins. `payment_hash` and
|
||||
`groupid` uniquely identify this xpay invocation, and `partid` then identifies
|
||||
this particular attempt to pay part of that.
|
||||
|
||||
`total_payment_msat` is the total amount (usually the invoice amount),
|
||||
which will be the same across all parts, adn `attempt_msat` is the
|
||||
amount being delivered to the destination by this part.
|
||||
|
||||
Each element in `hops` shows the amount going into the node (i.e. with
|
||||
fees, `channel_in_msat`) and the amount we're telling it to send
|
||||
to the other end (`channel_out_msat`). The `channel_out_msat` will
|
||||
be equal to the next `channel_in_msat. The final
|
||||
`channel_out_msat` will be equal to the `attempt_msat`.
|
||||
|
||||
The example shows a payment from this node via 103x1x0 (direction 1) to 022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59, then via 103x2x0 (direction 0) to 035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "pay_part_start",
|
||||
"params": {
|
||||
"origin": "cln-xpay",
|
||||
"payload": {
|
||||
"payment_hash": "651b28004d41cf0dc8e39a0b3d905651a7b012d03d81199fde09314700cb5a62",
|
||||
"groupid": 5793910575598463611,
|
||||
"partid": 1,
|
||||
"total_payment_msat": 5000000,
|
||||
"attempt_msat": 5000000,
|
||||
"hops": [
|
||||
{
|
||||
"next_node": "022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59",
|
||||
"short_channel_id": "103x1x0",
|
||||
"direction": 1,
|
||||
"channel_in_msat": 5000051,
|
||||
"channel_out_msat": 5000051
|
||||
},
|
||||
{
|
||||
"next_node": "035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d",
|
||||
"short_channel_id": "103x2x0",
|
||||
"direction": 0,
|
||||
"channel_in_msat": 5000051,
|
||||
"channel_out_msat": 5000000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `pay_part_end` (v25.09 onward)
|
||||
|
||||
Emitted by `xpay` when part of a payment ends. `payment_hash`, `groupid` and `partid`
|
||||
will match a previous `pay_part_start`.
|
||||
|
||||
`status` will be "success" or "failure". `duration` will be a number of seconds, with 9 decimal places. This is the time between `xpay` telling lightningd to send the onion, to when `xpay` processes the response.
|
||||
|
||||
If `status` is "failure", there will always be an `error_message`: the other fields below
|
||||
will be missing in the unusual case where the error onion is corrupted.
|
||||
|
||||
`failed_node_id`: If it's a non-local error, the source of the error.
|
||||
`failed_short_channel_id`: if it's not the final node, the channel it's complaining about.
|
||||
`failed_direction`: if it's not the final node, the channel direction.
|
||||
`failed_msg`: the decrypted onion message, in hex, if it was valid.
|
||||
`error_code`: the error code returned (present unless onion was corrupted).
|
||||
`error_message`: always present: if `failed_node_id` is present it's just the name of the `error_code`, but otherwise it can be a more informative error from our own node.
|
||||
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "pay_part_end",
|
||||
"params": {
|
||||
"origin": "cln-xpay",
|
||||
"payload": {
|
||||
"status": "success",
|
||||
"duration": 0.220209189,
|
||||
"payment_hash": "651b28004d41cf0dc8e39a0b3d905651a7b012d03d81199fde09314700cb5a62",
|
||||
"groupid": 5793910575598463611,
|
||||
"partid": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -149,6 +149,7 @@ struct attempt {
|
||||
|
||||
struct payment *payment;
|
||||
struct amount_msat delivers;
|
||||
struct timemono start_time;
|
||||
|
||||
/* Path we tried, so we can unreserve, and tell askrene the results */
|
||||
const struct hop *hops;
|
||||
@@ -536,6 +537,73 @@ static struct amount_msat total_delivered(const struct payment *payment)
|
||||
return sum;
|
||||
}
|
||||
|
||||
/* We can notify others of what the details are, so they can do their own
|
||||
* layer heuristics. */
|
||||
static void json_add_attempt_fields(struct json_stream *js,
|
||||
const struct attempt *attempt)
|
||||
{
|
||||
/* These three uniquely identify this attempt */
|
||||
json_add_sha256(js, "payment_hash", &attempt->payment->payment_hash);
|
||||
json_add_u64(js, "groupid", attempt->payment->group_id);
|
||||
json_add_u64(js, "partid", attempt->partid);
|
||||
}
|
||||
|
||||
static void outgoing_notify_start(const struct attempt *attempt)
|
||||
{
|
||||
struct json_stream *js = plugin_notification_start(NULL, "pay_part_start");
|
||||
json_add_attempt_fields(js, attempt);
|
||||
json_add_amount_msat(js, "total_payment_msat", attempt->payment->amount);
|
||||
json_add_amount_msat(js, "attempt_msat", attempt->delivers);
|
||||
json_array_start(js, "hops");
|
||||
for (size_t i = 0; i < tal_count(attempt->hops); i++) {
|
||||
const struct hop *hop = &attempt->hops[i];
|
||||
json_object_start(js, NULL);
|
||||
json_add_pubkey(js, "next_node", &hop->next_node);
|
||||
json_add_short_channel_id(js, "short_channel_id", hop->scidd.scid);
|
||||
json_add_u32(js, "direction", hop->scidd.dir);
|
||||
json_add_amount_msat(js, "channel_in_msat", hop->amount_in);
|
||||
json_add_amount_msat(js, "channel_out_msat", hop->amount_out);
|
||||
json_object_end(js);
|
||||
}
|
||||
json_array_end(js);
|
||||
plugin_notification_end(attempt->payment->plugin, js);
|
||||
}
|
||||
|
||||
static void outgoing_notify_success(const struct attempt *attempt)
|
||||
{
|
||||
struct json_stream *js = plugin_notification_start(NULL, "pay_part_end");
|
||||
json_add_string(js, "status", "success");
|
||||
json_add_timerel(js, "duration", timemono_between(time_mono(), attempt->start_time));
|
||||
json_add_attempt_fields(js, attempt);
|
||||
plugin_notification_end(attempt->payment->plugin, js);
|
||||
}
|
||||
|
||||
static void outgoing_notify_failure(const struct attempt *attempt,
|
||||
int failindex, int errcode,
|
||||
const u8 *replymsg,
|
||||
const char *errstr)
|
||||
{
|
||||
struct json_stream *js = plugin_notification_start(NULL, "pay_part_end");
|
||||
json_add_string(js, "status", "failure");
|
||||
json_add_attempt_fields(js, attempt);
|
||||
if (replymsg)
|
||||
json_add_hex_talarr(js, "failed_msg", replymsg);
|
||||
json_add_timerel(js, "duration", timemono_between(time_mono(), attempt->start_time));
|
||||
if (failindex != -1) {
|
||||
if (failindex != 0)
|
||||
json_add_pubkey(js, "failed_node_id", &attempt->hops[failindex-1].next_node);
|
||||
if (failindex != tal_count(attempt->hops)) {
|
||||
const struct hop *hop = &attempt->hops[failindex];
|
||||
json_add_short_channel_id(js, "failed_short_channel_id", hop->scidd.scid);
|
||||
json_add_u32(js, "failed_direction", hop->scidd.dir);
|
||||
}
|
||||
}
|
||||
if (errcode != -1)
|
||||
json_add_u32(js, "error_code", errcode);
|
||||
json_add_string(js, "error_message", errstr);
|
||||
plugin_notification_end(attempt->payment->plugin, js);
|
||||
}
|
||||
|
||||
static void update_knowledge_from_error(struct command *aux_cmd,
|
||||
const char *buf,
|
||||
const jsmntok_t *error,
|
||||
@@ -590,6 +658,7 @@ static void update_knowledge_from_error(struct command *aux_cmd,
|
||||
|
||||
/* Garbled? Blame random hop. */
|
||||
if (!replymsg) {
|
||||
outgoing_notify_failure(attempt, -1, -1, replymsg, "Garbled error message");
|
||||
index = pseudorand(tal_count(attempt->hops));
|
||||
description = "Garbled error message";
|
||||
add_result_summary(attempt, LOG_UNUSUAL,
|
||||
@@ -627,6 +696,7 @@ static void update_knowledge_from_error(struct command *aux_cmd,
|
||||
} else
|
||||
errmsg = failcode_name;
|
||||
|
||||
outgoing_notify_failure(attempt, index, failcode, replymsg, errmsg);
|
||||
description = tal_fmt(tmpctx,
|
||||
"Error %s for path %s, from %s",
|
||||
errmsg,
|
||||
@@ -881,6 +951,8 @@ static struct command_result *injectpaymentonion_succeeded(struct command *aux_c
|
||||
plugin_err(aux_cmd->plugin, "Invalid injectpaymentonion result '%.*s'",
|
||||
json_tok_full_len(result), json_tok_full(buf, result));
|
||||
|
||||
outgoing_notify_success(attempt);
|
||||
|
||||
/* Move from current_attempts to past_attempts */
|
||||
list_del_from(&payment->current_attempts, &attempt->list);
|
||||
list_add(&payment->past_attempts, &attempt->list);
|
||||
@@ -1008,6 +1080,9 @@ static struct command_result *do_inject(struct command *aux_cmd,
|
||||
return command_still_pending(aux_cmd);
|
||||
}
|
||||
|
||||
outgoing_notify_start(attempt);
|
||||
attempt->start_time = time_mono();
|
||||
|
||||
req = jsonrpc_request_start(aux_cmd,
|
||||
"injectpaymentonion",
|
||||
injectpaymentonion_succeeded,
|
||||
@@ -2118,6 +2193,12 @@ static const struct plugin_hook hooks[] = {
|
||||
},
|
||||
};
|
||||
|
||||
/* Notifications for each payment part we attempt */
|
||||
static const char *outgoing_notifications[] = {
|
||||
"pay_part_start",
|
||||
"pay_part_end",
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
struct xpay *xpay;
|
||||
@@ -2131,7 +2212,7 @@ int main(int argc, char *argv[])
|
||||
commands, ARRAY_SIZE(commands),
|
||||
notifications, ARRAY_SIZE(notifications),
|
||||
hooks, ARRAY_SIZE(hooks),
|
||||
NULL, 0,
|
||||
outgoing_notifications, ARRAY_SIZE(outgoing_notifications),
|
||||
plugin_option_dynamic("xpay-handle-pay", "bool",
|
||||
"Make xpay take over pay commands it can handle.",
|
||||
bool_option, bool_jsonfmt, &xpay->take_over_pay),
|
||||
|
||||
Reference in New Issue
Block a user