diff --git a/doc/lightning-listinvoices.7 b/doc/lightning-listinvoices.7 index 93ad4f9cd..ce7b90a4e 100644 --- a/doc/lightning-listinvoices.7 +++ b/doc/lightning-listinvoices.7 @@ -2,12 +2,12 @@ .\" Title: lightning-listinvoices .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets v1.79.1 -.\" Date: 01/16/2018 +.\" Date: 01/18/2018 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" -.TH "LIGHTNING\-LISTINVOI" "7" "01/16/2018" "\ \&" "\ \&" +.TH "LIGHTNING\-LISTINVOI" "7" "01/18/2018" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -37,7 +37,7 @@ lightning-listinvoices \- Protocol for querying invoice status The \fBlistinvoices\fR RPC command gets the status of a specific invoice, if it exists, or the status of all invoices if given no argument\&. .SH "RETURN VALUE" .sp -On success, an array \fIinvoices\fR of objects is returned\&. Each object contains \fIlabel\fR, \fIpayment_hash, \*(Aqcomplete\fR (a boolean), and \fIexpiry_time\fR (a UNIX timestamp)\&. If the \fImsatoshi\fR argument to lightning\-invoice(7) was not "any", there will be an \fImsatoshi\fR field\&. If the invoice has been paid, there will be a \fIpay_index\fR field and an \fImsatoshi_received\fR field (which may be slightly greater than \fImsatoshi\fR as some overpaying is permitted to allow clients to obscure payment paths)\&. +On success, an array \fIinvoices\fR of objects is returned\&. Each object contains \fIlabel\fR, \fIpayment_hash\fR, \fIstatus\fR (one of \fIunpaid\fR, \fIpaid\fR or \fIexpired\fR), and \fIexpiry_time\fR (a UNIX timestamp)\&. If the \fImsatoshi\fR argument to lightning\-invoice(7) was not "any", there will be an \fImsatoshi\fR field\&. If the invoice \fIstatus\fR is \fIpaid\fR, there will be a \fIpay_index\fR field and an \fImsatoshi_received\fR field (which may be slightly greater than \fImsatoshi\fR as some overpaying is permitted to allow clients to obscure payment paths)\&. .SH "AUTHOR" .sp Rusty Russell is mainly responsible\&. diff --git a/doc/lightning-listinvoices.7.txt b/doc/lightning-listinvoices.7.txt index 2ec20961c..4579a1c66 100644 --- a/doc/lightning-listinvoices.7.txt +++ b/doc/lightning-listinvoices.7.txt @@ -17,7 +17,7 @@ it exists, or the status of all invoices if given no argument. RETURN VALUE ------------ -On success, an array 'invoices' of objects is returned. Each object contains 'label', 'payment_hash, 'complete' (a boolean), and 'expiry_time' (a UNIX timestamp). If the 'msatoshi' argument to lightning-invoice(7) was not "any", there will be an 'msatoshi' field. If the invoice has been paid, there will be a 'pay_index' field and an 'msatoshi_received' field (which may be slightly greater than 'msatoshi' as some overpaying is permitted to allow clients to obscure payment paths). +On success, an array 'invoices' of objects is returned. Each object contains 'label', 'payment_hash', 'status' (one of 'unpaid', 'paid' or 'expired'), and 'expiry_time' (a UNIX timestamp). If the 'msatoshi' argument to lightning-invoice(7) was not "any", there will be an 'msatoshi' field. If the invoice 'status' is 'paid', there will be a 'pay_index' field and an 'msatoshi_received' field (which may be slightly greater than 'msatoshi' as some overpaying is permitted to allow clients to obscure payment paths). //FIXME:Enumerate errors diff --git a/lightningd/invoice.c b/lightningd/invoice.c index 69f7625ff..eb94c548d 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -15,18 +15,32 @@ #include #include #include +#include #include #include +static const char *invoice_status_str(const struct invoice *inv) +{ + if (inv->state == PAID) + return "paid"; + if (time_now().ts.tv_sec > inv->expiry_time) + return "expired"; + return "unpaid"; +} + static void json_add_invoice(struct json_result *response, - const struct invoice *inv) + const struct invoice *inv, + bool modern) { json_object_start(response, NULL); json_add_string(response, "label", inv->label); json_add_hex(response, "payment_hash", &inv->rhash, sizeof(inv->rhash)); if (inv->msatoshi) json_add_u64(response, "msatoshi", *inv->msatoshi); - json_add_bool(response, "complete", inv->state == PAID); + if (modern) + json_add_string(response, "status", invoice_status_str(inv)); + else if (deprecated_apis && !modern) + json_add_bool(response, "complete", inv->state == PAID); if (inv->state == PAID) { json_add_u64(response, "pay_index", inv->pay_index); json_add_u64(response, "msatoshi_received", @@ -42,7 +56,7 @@ static void tell_waiter(struct command *cmd, const struct invoice *paid) { struct json_result *response = new_json_result(cmd); - json_add_invoice(response, paid); + json_add_invoice(response, paid, true); command_success(cmd, response); } static void tell_waiter_deleted(struct command *cmd) @@ -183,7 +197,8 @@ AUTODATA(json_command, &invoice_command); static void json_add_invoices(struct json_result *response, struct wallet *wallet, - const char *buffer, const jsmntok_t *label) + const char *buffer, const jsmntok_t *label, + bool modern) { const struct invoice *i; char *lbl = NULL; @@ -194,7 +209,7 @@ static void json_add_invoices(struct json_result *response, while ((i = wallet_invoice_iterate(wallet, i)) != NULL) { if (lbl && !streq(i->label, lbl)) continue; - json_add_invoice(response, i); + json_add_invoice(response, i, modern); } } @@ -219,7 +234,7 @@ static void json_listinvoice_internal(struct command *cmd, json_array_start(response, "invoices"); } else json_array_start(response, NULL); - json_add_invoices(response, wallet, buffer, label); + json_add_invoices(response, wallet, buffer, label, modern); json_array_end(response); if (modern) json_object_end(response); @@ -283,7 +298,7 @@ static void json_delinvoice(struct command *cmd, /* Get invoice details before attempting to delete, as * otherwise the invoice will be freed. */ - json_add_invoice(response, i); + json_add_invoice(response, i, true); error = wallet_invoice_delete(wallet, i); diff --git a/tests/test_lightningd.py b/tests/test_lightningd.py index 50a18568a..1ebbbad58 100644 --- a/tests/test_lightningd.py +++ b/tests/test_lightningd.py @@ -280,7 +280,7 @@ class LightningDTests(BaseLightningDTests): label = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20)) rhash = ldst.rpc.invoice(amt, label, label)['payment_hash'] - assert ldst.rpc.listinvoices(label)['invoices'][0]['complete'] == False + assert ldst.rpc.listinvoices(label)['invoices'][0]['status'] == 'unpaid' routestep = { 'msatoshi' : amt, @@ -292,7 +292,7 @@ class LightningDTests(BaseLightningDTests): def wait_pay(): # Up to 10 seconds for payment to succeed. start_time = time.time() - while not ldst.rpc.listinvoices(label)['invoices'][0]['complete']: + while ldst.rpc.listinvoices(label)['invoices'][0]['status'] != 'paid': if time.time() > start_time + 10: raise TimeoutError('Payment timed out') time.sleep(0.1) @@ -366,7 +366,7 @@ class LightningDTests(BaseLightningDTests): inv = l2.rpc.invoice(123000, 'test_pay', 'description', 1)['bolt11'] time.sleep(2) self.assertRaises(ValueError, l1.rpc.pay, inv) - assert l2.rpc.listinvoices('test_pay')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('test_pay')['invoices'][0]['status'] == 'expired' assert l2.rpc.listinvoices('test_pay')['invoices'][0]['expiry_time'] < time.time() def test_connect(self): @@ -639,7 +639,7 @@ class LightningDTests(BaseLightningDTests): amt = 200000000 rhash = l2.rpc.invoice(amt, 'testpayment2', 'desc')['payment_hash'] - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'unpaid' routestep = { 'msatoshi' : amt, @@ -652,25 +652,25 @@ class LightningDTests(BaseLightningDTests): rs = copy.deepcopy(routestep) rs['msatoshi'] = rs['msatoshi'] - 1 self.assertRaises(ValueError, l1.rpc.sendpay, to_json([rs]), rhash) - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'unpaid' # Gross overpayment (more than factor of 2) rs = copy.deepcopy(routestep) rs['msatoshi'] = rs['msatoshi'] * 2 + 1 self.assertRaises(ValueError, l1.rpc.sendpay, to_json([rs]), rhash) - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'unpaid' # Insufficient delay. rs = copy.deepcopy(routestep) rs['delay'] = rs['delay'] - 2 self.assertRaises(ValueError, l1.rpc.sendpay, to_json([rs]), rhash) - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'unpaid' # Bad ID. rs = copy.deepcopy(routestep) rs['id'] = '00000000000000000000000000000000' self.assertRaises(ValueError, l1.rpc.sendpay, to_json([rs]), rhash) - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'unpaid' # FIXME: test paying via another node, should fail to pay twice. p1 = l1.rpc.getpeer(l2.info['id'], 'info') @@ -682,7 +682,7 @@ class LightningDTests(BaseLightningDTests): # This works. preimage2 = l1.rpc.sendpay(to_json([routestep]), rhash) - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == True + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'paid' assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['pay_index'] == 1 assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['msatoshi_received'] == rs['msatoshi'] @@ -700,15 +700,15 @@ class LightningDTests(BaseLightningDTests): preimage = l1.rpc.sendpay(to_json([routestep]), rhash) assert preimage == preimage2 l1.daemon.wait_for_log('... succeeded') - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == True + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'paid' assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['msatoshi_received'] == rs['msatoshi'] # Overpaying by "only" a factor of 2 succeeds. rhash = l2.rpc.invoice(amt, 'testpayment3', 'desc')['payment_hash'] - assert l2.rpc.listinvoices('testpayment3')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment3')['invoices'][0]['status'] == 'unpaid' routestep = { 'msatoshi' : amt * 2, 'id' : l2.info['id'], 'delay' : 5, 'channel': '1:1:1'} preimage3 = l1.rpc.sendpay(to_json([routestep]), rhash) - assert l2.rpc.listinvoices('testpayment3')['invoices'][0]['complete'] == True + assert l2.rpc.listinvoices('testpayment3')['invoices'][0]['status'] == 'paid' assert l2.rpc.listinvoices('testpayment3')['invoices'][0]['msatoshi_received'] == amt*2 # Test listpayments @@ -769,7 +769,8 @@ class LightningDTests(BaseLightningDTests): preimage = l1.rpc.pay(inv) after = int(time.time()) invoice = l2.rpc.listinvoices('test_pay')['invoices'][0] - assert invoice['complete'] == True + print(invoice) + assert invoice['status'] == 'paid' assert invoice['paid_timestamp'] >= before assert invoice['paid_timestamp'] <= after @@ -1001,7 +1002,7 @@ class LightningDTests(BaseLightningDTests): l1.daemon.wait_for_log('onchaind complete, forgetting peer') # Payment failed, BTW - assert l2.rpc.listinvoices('onchain_dust_out')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('onchain_dust_out')['invoices'][0]['status'] == 'unpaid' @unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") def test_onchain_timeout(self): @@ -1064,7 +1065,7 @@ class LightningDTests(BaseLightningDTests): l1.daemon.wait_for_log('onchaind complete, forgetting peer') # Payment failed, BTW - assert l2.rpc.listinvoices('onchain_timeout')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('onchain_timeout')['invoices'][0]['status'] == 'unpaid' @unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") def test_onchain_middleman(self): @@ -1642,7 +1643,7 @@ class LightningDTests(BaseLightningDTests): assert l3.rpc.getpeer(l2.info['id'])['channel'] == chanid2 rhash = l3.rpc.invoice(100000000, 'testpayment1', 'desc')['payment_hash'] - assert l3.rpc.listinvoices('testpayment1')['invoices'][0]['complete'] == False + assert l3.rpc.listinvoices('testpayment1')['invoices'][0]['status'] == 'unpaid' # Fee for node2 is 10 millionths, plus 1. amt = 100000000 @@ -1783,7 +1784,7 @@ class LightningDTests(BaseLightningDTests): assert route[1]['delay'] == 9 + shadow_route rhash = l3.rpc.invoice(4999999, 'test_forward_different_fees_and_cltv', 'desc')['payment_hash'] - assert l3.rpc.listinvoices('test_forward_different_fees_and_cltv')['invoices'][0]['complete'] == False + assert l3.rpc.listinvoices('test_forward_different_fees_and_cltv')['invoices'][0]['status'] == 'unpaid' # This should work. l1.rpc.sendpay(to_json(route), rhash) @@ -1796,7 +1797,7 @@ class LightningDTests(BaseLightningDTests): .format(bitcoind.rpc.getblockcount() + 9 + shadow_route)) l3.daemon.wait_for_log("test_forward_different_fees_and_cltv: Actual amount 4999999msat, HTLC expiry {}" .format(bitcoind.rpc.getblockcount() + 9 + shadow_route)) - assert l3.rpc.listinvoices('test_forward_different_fees_and_cltv')['invoices'][0]['complete'] == True + assert l3.rpc.listinvoices('test_forward_different_fees_and_cltv')['invoices'][0]['status'] == 'paid' # Check that we see all the channels shortids = set(c['short_channel_id'] for c in l2.rpc.listchannels()['channels']) @@ -1852,7 +1853,7 @@ class LightningDTests(BaseLightningDTests): # This should work. rhash = l3.rpc.invoice(4999999, 'test_forward_pad_fees_and_cltv', 'desc')['payment_hash'] l1.rpc.sendpay(to_json(route), rhash) - assert l3.rpc.listinvoices('test_forward_pad_fees_and_cltv')['invoices'][0]['complete'] == True + assert l3.rpc.listinvoices('test_forward_pad_fees_and_cltv')['invoices'][0]['status'] == 'paid' @unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") def test_htlc_out_timeout(self): @@ -1872,7 +1873,7 @@ class LightningDTests(BaseLightningDTests): amt = 200000000 inv = l2.rpc.invoice(amt, 'test_htlc_out_timeout', 'desc')['bolt11'] - assert l2.rpc.listinvoices('test_htlc_out_timeout')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('test_htlc_out_timeout')['invoices'][0]['status'] == 'unpaid' payfuture = self.executor.submit(l1.rpc.pay, inv) @@ -1927,7 +1928,7 @@ class LightningDTests(BaseLightningDTests): amt = 200000000 inv = l2.rpc.invoice(amt, 'test_htlc_in_timeout', 'desc')['bolt11'] - assert l2.rpc.listinvoices('test_htlc_in_timeout')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('test_htlc_in_timeout')['invoices'][0]['status'] == 'unpaid' payfuture = self.executor.submit(l1.rpc.pay, inv) @@ -2126,7 +2127,7 @@ class LightningDTests(BaseLightningDTests): amt = 200000000 rhash = l2.rpc.invoice(amt, 'test_reconnect_sender_add1', 'desc')['payment_hash'] - assert l2.rpc.listinvoices('test_reconnect_sender_add1')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('test_reconnect_sender_add1')['invoices'][0]['status'] == 'unpaid' route = [ { 'msatoshi' : amt, 'id' : l2.info['id'], 'delay' : 5, 'channel': '1:1:1'} ] @@ -2154,7 +2155,7 @@ class LightningDTests(BaseLightningDTests): amt = 200000000 rhash = l2.rpc.invoice(amt, 'testpayment', 'desc')['payment_hash'] - assert l2.rpc.listinvoices('testpayment')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment')['invoices'][0]['status'] == 'unpaid' route = [ { 'msatoshi' : amt, 'id' : l2.info['id'], 'delay' : 5, 'channel': '1:1:1'} ] @@ -2180,13 +2181,13 @@ class LightningDTests(BaseLightningDTests): amt = 200000000 rhash = l2.rpc.invoice(amt, 'testpayment2', 'desc')['payment_hash'] - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'unpaid' route = [ { 'msatoshi' : amt, 'id' : l2.info['id'], 'delay' : 5, 'channel': '1:1:1'} ] l1.rpc.sendpay(to_json(route), rhash) for i in range(len(disconnects)): l1.daemon.wait_for_log('Already have funding locked in') - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == True + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'paid' @unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") def test_reconnect_receiver_fulfill(self): @@ -2210,13 +2211,13 @@ class LightningDTests(BaseLightningDTests): amt = 200000000 rhash = l2.rpc.invoice(amt, 'testpayment2', 'desc')['payment_hash'] - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'unpaid' route = [ { 'msatoshi' : amt, 'id' : l2.info['id'], 'delay' : 5, 'channel': '1:1:1'} ] l1.rpc.sendpay(to_json(route), rhash) for i in range(len(disconnects)): l1.daemon.wait_for_log('Already have funding locked in') - assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['complete'] == True + assert l2.rpc.listinvoices('testpayment2')['invoices'][0]['status'] == 'paid' @unittest.skipIf(not DEVELOPER, "needs DEVELOPER=1") def test_shutdown_reconnect(self): @@ -2586,7 +2587,7 @@ class LightningDTests(BaseLightningDTests): wait_for(lambda: l1.rpc.listpayments()['payments'][0]['status'] != 'pending') assert l1.rpc.listpayments()['payments'][0]['status'] == 'complete' - assert l2.rpc.listinvoices('inv1')['invoices'][0]['complete'] == True + assert l2.rpc.listinvoices('inv1')['invoices'][0]['status'] == 'paid' # FIXME: We should re-add pre-announced routes on startup! self.wait_for_routes(l1, [chanid]) @@ -2629,7 +2630,7 @@ class LightningDTests(BaseLightningDTests): wait_for(lambda: l1.rpc.listpayments()['payments'][0]['status'] != 'pending') - assert l2.rpc.listinvoices('inv1')['invoices'][0]['complete'] == False + assert l2.rpc.listinvoices('inv1')['invoices'][0]['status'] == 'expired' assert l1.rpc.listpayments()['payments'][0]['status'] == 'failed' # Another attempt should also fail.