diff --git a/CHANGELOG.md b/CHANGELOG.md index 97b5e7891162..44e7874f2bf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - bolt11: support for parsing feature bits (field `9`). - Protocol: we now retransmit `funding_locked` upon reconnection while closing if there was no update +- Plugin: new notifications `sendpay_success` and `sendpay_failure`. + ### Changed - JSON API: `txprepare` now uses `outputs` as parameter other than `destination` and `satoshi` diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index bf6f1f9b2c2b..44861a5bdad7 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -384,6 +384,68 @@ or only `settled` and `failed` case contain `resolved_time`; - The `failcode` and `failreason` are defined in [BOLT 4][bolt4-failure-codes]. +#### `sendpay_success` + +A notification for topic `sendpay_success` is sent every time a sendpay +success(with `complete` status). The json is same as the return value of +command `sendpay`/`waitsendpay` when these cammand succeeds. + +```json +{ + "sendpay_success": { + "id": 1, + "payment_hash": "5c85bf402b87d4860f4a728e2e58a2418bda92cd7aea0ce494f11670cfbfb206", + "destination": "035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d", + "msatoshi": 100000000, + "amount_msat": "100000000msat", + "msatoshi_sent": 100001001, + "amount_sent_msat": "100001001msat", + "created_at": 1561390572, + "status": "complete", + "payment_preimage": "9540d98095fd7f37687ebb7759e733934234d4f934e34433d4998a37de3733ee" + } +} +``` +`sendpay` doesn't wait for the result of sendpay and `waitsendpay` +returns the result of sendpay in specified time or timeout, but +`sendpay_success` will always return the result anytime when sendpay +successes if is was subscribed. + +#### `sendpay_failure` + +A notification for topic `sendpay_failure` is sent every time a sendpay +success(with `failed` status). The json is same as the return value of +command `sendpay`/`waitsendpay` when this cammand fails. + +```json +{ + "sendpay_failure": { + "code": 204, + "message": "failed: WIRE_UNKNOWN_NEXT_PEER (reply from remote)", + "data": { + "id": 2, + "payment_hash": "9036e3bdbd2515f1e653cb9f22f8e4c49b73aa2c36e937c926f43e33b8db8851", + "destination": "035d2b1192dfba134e10e540875d366ebc8bc353d5aa766b80c090b39c3a5d885d", + "msatoshi": 100000000, + "amount_msat": "100000000msat", + "msatoshi_sent": 100001001, + "amount_sent_msat": "100001001msat", + "created_at": 1561395134, + "status": "failed", + "erring_index": 1, + "failcode": 16394, + "failcodename": "WIRE_UNKNOWN_NEXT_PEER", + "erring_node": "022d223620a359a47ff7f7ac447c85c46c923da53389221a0054c11c1e3ca31d59", + "erring_channel": "103x2x1", + "erring_direction": 0 + } + } +} +``` +`sendpay` doesn't wait for the result of sendpay and `waitsendpay` +returns the result of sendpay in specified time or timeout, but +`sendpay_failure` will always return the result anytime when sendpay +fails if is was subscribed. ## Hooks diff --git a/lightningd/notification.c b/lightningd/notification.c index f17750287eb1..4877805d537b 100644 --- a/lightningd/notification.c +++ b/lightningd/notification.c @@ -227,3 +227,76 @@ void notify_forward_event(struct lightningd *ld, jsonrpc_notification_end(n); plugins_notify(ld->plugins, take(n)); } + +static void sendpay_success_notification_serialize(struct json_stream *stream, + const struct wallet_payment *payment) +{ + json_object_start(stream, "sendpay_success"); + json_add_payment_fields(stream, payment); + json_object_end(stream); /* .sendpay_success */ +} + +REGISTER_NOTIFICATION(sendpay_success, + sendpay_success_notification_serialize); + +void notify_sendpay_success(struct lightningd *ld, + const struct wallet_payment *payment) +{ + void (*serialize)(struct json_stream *, + const struct wallet_payment *) = sendpay_success_notification_gen.serialize; + + struct jsonrpc_notification *n = + jsonrpc_notification_start(NULL, "sendpay_success"); + serialize(n->stream, payment); + jsonrpc_notification_end(n); + plugins_notify(ld->plugins, take(n)); +} + +static void sendpay_failure_notification_serialize(struct json_stream *stream, + const struct wallet_payment *payment, + int pay_errcode, + const u8 *onionreply, + const struct routing_failure *fail, + char *errmsg) +{ + json_object_start(stream, "sendpay_failure"); + + /* In line with the format of json error returned + * by sendpay_fail(). */ + json_add_member(stream, "code", false, "%d", pay_errcode); + json_add_string(stream, "message", errmsg); + + json_object_start(stream, "data"); + json_sendpay_fail_fields(stream, + payment, + pay_errcode, + onionreply, + fail); + + json_object_end(stream); /* .data */ + json_object_end(stream); /* .sendpay_failure */ +} + +REGISTER_NOTIFICATION(sendpay_failure, + sendpay_failure_notification_serialize); + +void notify_sendpay_failure(struct lightningd *ld, + const struct wallet_payment *payment, + int pay_errcode, + const u8 *onionreply, + const struct routing_failure *fail, + char *errmsg) +{ + void (*serialize)(struct json_stream *, + const struct wallet_payment *, + int, + const u8 *, + const struct routing_failure *, + char *) = sendpay_failure_notification_gen.serialize; + + struct jsonrpc_notification *n = + jsonrpc_notification_start(NULL, "sendpay_failure"); + serialize(n->stream, payment, pay_errcode, onionreply, fail, errmsg); + jsonrpc_notification_end(n); + plugins_notify(ld->plugins, take(n)); +} diff --git a/lightningd/notification.h b/lightningd/notification.h index cf97d81bf4d7..95026d08e0a5 100644 --- a/lightningd/notification.h +++ b/lightningd/notification.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -54,4 +55,14 @@ void notify_forward_event(struct lightningd *ld, enum onion_type failcode, struct timeabs *resolved_time); +void notify_sendpay_success(struct lightningd *ld, + const struct wallet_payment *payment); + +void notify_sendpay_failure(struct lightningd *ld, + const struct wallet_payment *payment, + int pay_errcode, + const u8 *onionreply, + const struct routing_failure *fail, + char *errmsg); + #endif /* LIGHTNING_LIGHTNINGD_NOTIFICATION_H */ diff --git a/lightningd/pay.c b/lightningd/pay.c index ff52b9aa2f23..840376d63a1d 100644 --- a/lightningd/pay.c +++ b/lightningd/pay.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -73,9 +74,8 @@ add_waitsendpay_waiter(struct lightningd *ld, } /* Outputs fields, not a separate object*/ -static void -json_add_payment_fields(struct json_stream *response, - const struct wallet_payment *t) +void json_add_payment_fields(struct json_stream *response, + const struct wallet_payment *t) { json_add_u64(response, "id", t->id); json_add_sha256(response, "payment_hash", &t->payment_hash); @@ -114,6 +114,7 @@ static struct command_result *sendpay_success(struct command *cmd, assert(payment->status == PAYMENT_COMPLETE); + notify_sendpay_success(cmd->ld, payment); response = json_stream_success(cmd); json_add_payment_fields(response, payment); return command_success(cmd, response); @@ -142,36 +143,62 @@ json_add_routefail_info(struct json_stream *js, json_add_hex_talarr(js, "raw_message", msg); } +void json_sendpay_fail_fields(struct json_stream *js, + const struct wallet_payment *payment, + int pay_errcode, + const u8 *onionreply, + const struct routing_failure *fail) +{ + /* "immediate_routing_failure" is before payment creation. */ + if (payment) + json_add_payment_fields(js, payment); + if (pay_errcode == PAY_UNPARSEABLE_ONION) + json_add_hex_talarr(js, "onionreply", onionreply); + else + json_add_routefail_info(js, + fail->erring_index, + fail->failcode, + &fail->erring_node, + &fail->erring_channel, + fail->channel_dir, + fail->msg); +} + /* onionreply used if pay_errcode == PAY_UNPARSEABLE_ONION */ static struct command_result * sendpay_fail(struct command *cmd, + const struct wallet_payment *payment, int pay_errcode, const u8 *onionreply, const struct routing_failure *fail, const char *details) { struct json_stream *data; - - if (pay_errcode == PAY_UNPARSEABLE_ONION) { - data = json_stream_fail(cmd, PAY_UNPARSEABLE_ONION, - "Malformed error reply"); - json_add_hex_talarr(data, "onionreply", onionreply); - json_object_end(data); - return command_failed(cmd, data); + char *errmsg; + + if (pay_errcode == PAY_UNPARSEABLE_ONION) + errmsg = "Malformed error reply"; + else { + assert(fail); + errmsg = tal_fmt(tmpctx, "failed: %s (%s)", + onion_type_name(fail->failcode), + details); } - assert(fail); + notify_sendpay_failure(cmd->ld, + payment, + pay_errcode, + onionreply, + fail, + errmsg); + data = json_stream_fail(cmd, pay_errcode, - tal_fmt(tmpctx, "failed: %s (%s)", - onion_type_name(fail->failcode), - details)); - json_add_routefail_info(data, - fail->erring_index, - fail->failcode, - &fail->erring_node, - &fail->erring_channel, - fail->channel_dir, - fail->msg); + errmsg); + json_sendpay_fail_fields(data, + payment, + pay_errcode, + onionreply, + fail); json_object_end(data); return command_failed(cmd, data); } @@ -190,6 +217,7 @@ json_sendpay_in_progress(struct command *cmd, static void tell_waiters_failed(struct lightningd *ld, const struct sha256 *payment_hash, + const struct wallet_payment *payment, int pay_errcode, const u8 *onionreply, const struct routing_failure *fail, @@ -203,7 +231,8 @@ static void tell_waiters_failed(struct lightningd *ld, if (!sha256_eq(payment_hash, &pc->payment_hash)) continue; - sendpay_fail(pc->cmd, pay_errcode, onionreply, fail, details); + sendpay_fail(pc->cmd, payment, + pay_errcode, onionreply, fail, details); } } @@ -498,11 +527,11 @@ void payment_failed(struct lightningd *ld, const struct htlc_out *hout, failmsg, fail ? fail->channel_dir : 0); - tell_waiters_failed(ld, &hout->payment_hash, pay_errcode, - hout->failuremsg, fail, failmsg); + tell_waiters_failed(ld, &hout->payment_hash, payment, + pay_errcode, hout->failuremsg, fail, failmsg); } -/* Wait for a payment. If cmd is deleted, then json_waitsendpay_on_resolve +/* Wait for a payment. If cmd is deleted, then wait_payment() * no longer be called. * Return callback if we called already, otherwise NULL. */ static struct command_result *wait_payment(struct lightningd *ld, @@ -555,7 +584,9 @@ static struct command_result *wait_payment(struct lightningd *ld, "Payment failure reason unknown"); } else if (failonionreply) { /* failed to parse returned onion error */ - return sendpay_fail(cmd, PAY_UNPARSEABLE_ONION, + return sendpay_fail(cmd, + payment, + PAY_UNPARSEABLE_ONION, failonionreply, NULL, faildetail); } else { @@ -571,6 +602,7 @@ static struct command_result *wait_payment(struct lightningd *ld, /* FIXME: We don't store this! */ fail->msg = NULL; return sendpay_fail(cmd, + payment, faildestperm ? PAY_DESTINATION_PERM_FAIL : PAY_TRY_OTHER_ROUTE, @@ -709,8 +741,8 @@ send_payment(struct lightningd *ld, &route[0].channel_id, &channel->peer->id); - return sendpay_fail(cmd, PAY_TRY_OTHER_ROUTE, NULL, - fail, "First peer not ready"); + return sendpay_fail(cmd, payment, PAY_TRY_OTHER_ROUTE, + NULL, fail, "First peer not ready"); } /* Copy channels used along the route. */ diff --git a/lightningd/pay.h b/lightningd/pay.h index a3edef862d15..53f0e0cdd77a 100644 --- a/lightningd/pay.h +++ b/lightningd/pay.h @@ -1,11 +1,15 @@ #ifndef LIGHTNING_LIGHTNINGD_PAY_H #define LIGHTNING_LIGHTNINGD_PAY_H #include "config.h" +#include struct htlc_out; struct lightningd; struct preimage; struct sha256; +struct json_stream; +struct wallet_payment; +struct routing_failure; void payment_succeeded(struct lightningd *ld, struct htlc_out *hout, const struct preimage *rval); @@ -16,4 +20,15 @@ void payment_failed(struct lightningd *ld, const struct htlc_out *hout, /* Inform payment system to save the payment. */ void payment_store(struct lightningd *ld, const struct sha256 *payment_hash); +/* This json will be also used in 'sendpay_success' notifictaion. */ +void json_add_payment_fields(struct json_stream *response, + const struct wallet_payment *t); + +/* This json will be also used in 'sendpay_failure' notifictaion. */ +void json_sendpay_fail_fields(struct json_stream *js, + const struct wallet_payment *t, + int pay_errcode, + const u8 *onionreply, + const struct routing_failure *fail); + #endif /* LIGHTNING_LIGHTNINGD_PAY_H */ diff --git a/tests/plugins/sendpay_notifications.py b/tests/plugins/sendpay_notifications.py new file mode 100755 index 000000000000..35d3058fc80d --- /dev/null +++ b/tests/plugins/sendpay_notifications.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""This plugin is used to check that sendpay_success and sendpay_failure calls are working correctly. +""" +from lightning import Plugin + +plugin = Plugin() + + +@plugin.init() +def init(configuration, options, plugin): + plugin.success_list = [] + plugin.failure_list = [] + + +@plugin.subscribe("sendpay_success") +def notify_sendpay_success(plugin, sendpay_success): + plugin.log("receive a sendpay_success recored, id: {}, payment_hash: {}".format(sendpay_success['id'], sendpay_success['payment_hash'])) + plugin.success_list.append(sendpay_success) + + +@plugin.subscribe("sendpay_failure") +def notify_sendpay_failure(plugin, sendpay_failure): + plugin.log("receive a sendpay_failure recored, id: {}, payment_hash: {}".format(sendpay_failure['data']['id'], + sendpay_failure['data']['payment_hash'])) + plugin.failure_list.append(sendpay_failure) + + +@plugin.method('listsendpays_plugin') +def record_lookup(plugin): + return {'sendpay_success': plugin.success_list, + 'sendpay_failure': plugin.failure_list} + + +plugin.run() diff --git a/tests/test_plugin.py b/tests/test_plugin.py index cbe43cdcdb61..b6cc95149cbf 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -708,3 +708,34 @@ def test_plugin_deprecated_relpath(node_factory): assert l1.daemon.is_in_log('DEPRECATED WARNING.*plugin={}' .format(os.path.join(os.getcwd(), 'tests/plugins/millisatoshis.py'))) + + +def test_sendpay_notifications(node_factory, bitcoind): + """ test 'sendpay_success' and 'sendpay_failure' notifications + """ + amount = 10**8 + opts = [{'plugin': os.path.join(os.getcwd(), 'tests/plugins/sendpay_notifications.py')}, + {}, + {'may_reconnect': False}] + l1, l2, l3 = node_factory.line_graph(3, opts=opts, wait_for_announce=True) + chanid23 = l2.get_channel_scid(l3) + + payment_hash1 = l3.rpc.invoice(amount, "first", "desc")['payment_hash'] + payment_hash2 = l3.rpc.invoice(amount, "second", "desc")['payment_hash'] + route = l1.rpc.getroute(l3.info['id'], amount, 1)['route'] + + l1.rpc.sendpay(route, payment_hash1) + response1 = l1.rpc.waitsendpay(payment_hash1) + + l2.rpc.close(chanid23, 1) + + l1.rpc.sendpay(route, payment_hash2) + with pytest.raises(RpcError) as err: + l1.rpc.waitsendpay(payment_hash2) + + results = l1.rpc.call('listsendpays_plugin') + assert len(results['sendpay_success']) == 1 + assert len(results['sendpay_failure']) == 1 + + assert results['sendpay_success'][0] == response1 + assert results['sendpay_failure'][0] == err.value.error