diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index f7cb09776..47a56e69e 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -77,6 +77,7 @@ static const errcode_t INVOICE_WAIT_TIMED_OUT = 904; static const errcode_t INVOICE_NOT_FOUND = 905; static const errcode_t INVOICE_STATUS_UNEXPECTED = 906; static const errcode_t INVOICE_OFFER_INACTIVE = 907; +static const errcode_t INVOICE_NO_DESCRIPTION = 908; /* Errors from HSM crypto operations. */ static const errcode_t HSM_ECDH_FAILED = 800; diff --git a/contrib/pyln-client/pyln/client/lightning.py b/contrib/pyln-client/pyln/client/lightning.py index ed05203f5..f7567fcf5 100644 --- a/contrib/pyln-client/pyln/client/lightning.py +++ b/contrib/pyln-client/pyln/client/lightning.py @@ -566,13 +566,14 @@ class LightningRpc(UnixDomainSocketRpc): } return self.call("delexpiredinvoice", payload) - def delinvoice(self, label, status): + def delinvoice(self, label, status, desconly=None): """ - Delete unpaid invoice {label} with {status}. + Delete unpaid invoice {label} with {status} (or, with {desconly} true, remove its description). """ payload = { "label": label, - "status": status + "status": status, + "desconly": desconly, } return self.call("delinvoice", payload) diff --git a/doc/lightning-delinvoice.7.md b/doc/lightning-delinvoice.7.md index 935c3e80d..fe446756a 100644 --- a/doc/lightning-delinvoice.7.md +++ b/doc/lightning-delinvoice.7.md @@ -1,20 +1,24 @@ -lightning-delinvoice -- Command for removing an invoice -======================================================= +lightning-delinvoice -- Command for removing an invoice (or just its description) +================================================================================= SYNOPSIS -------- -**delinvoice** *label* *status* +**delinvoice** *label* *status* [*desconly*] DESCRIPTION ----------- The **delinvoice** RPC command removes an invoice with *status* as given -in **listinvoices**. +in **listinvoices**, or with *desconly* set, removes its description. The caller should be particularly aware of the error case caused by the *status* changing just before this command is invoked! +If *desconly* is set, the invoice is not deleted, but has its +description removed (this can save space with very large descriptions, +as would be used with lightning-invoice(7) *deschashonly*. + RETURN VALUE ------------ @@ -55,6 +59,7 @@ The following errors may be reported: *current_status* and *expected_status* fields. This is most likely due to the *status* of the invoice changing just before this command is invoked. +- 908: The invoice already has no description, and *desconly* was set. AUTHOR ------ @@ -73,4 +78,4 @@ RESOURCES Main web site: -[comment]: # ( SHA256STAMP:cd3b009a6ef0c220ca21c6d8e3a5716ca2080997016cf00a2e26defc03cfac73) +[comment]: # ( SHA256STAMP:73d9097734e85d438de90844ab78bdd737e6df53620542887170c7564a86b90b) diff --git a/doc/lightning-listinvoices.7.md b/doc/lightning-listinvoices.7.md index 059d460fd..63cfe33d1 100644 --- a/doc/lightning-listinvoices.7.md +++ b/doc/lightning-listinvoices.7.md @@ -23,10 +23,10 @@ RETURN VALUE [comment]: # (GENERATE-FROM-SCHEMA-START) On success, an object containing **invoices** is returned. It is an array of objects, where each object contains: - **label** (string): unique label supplied at invoice creation -- **description** (string): description used in the invoice - **payment_hash** (hex): the hash of the *payment_preimage* which will prove payment (always 64 characters) - **status** (string): Whether it's paid, unpaid or unpayable (one of "unpaid", "paid", "expired") - **expires_at** (u64): UNIX timestamp of when it will become / became unpayable +- **description** (string, optional): description used in the invoice - **amount_msat** (msat, optional): the amount required to pay this invoice - **bolt11** (string, optional): the BOLT11 string (always present unless *bolt12* is) - **bolt12** (string, optional): the BOLT12 string (always present unless *bolt11* is) @@ -56,4 +56,4 @@ RESOURCES Main web site: -[comment]: # ( SHA256STAMP:3dc5d5b8f7796d29e0d174d96e93915cbc7131b173a1547de022e021c55e8db6) +[comment]: # ( SHA256STAMP:d1328ecc2a4e76ede8c9adc3a63d18ce36be305ddcee7cf717039f79642cfd41) diff --git a/doc/schemas/listinvoices.schema.json b/doc/schemas/listinvoices.schema.json index bbd505684..9afd91299 100644 --- a/doc/schemas/listinvoices.schema.json +++ b/doc/schemas/listinvoices.schema.json @@ -13,7 +13,6 @@ "additionalProperties": true, "required": [ "label", - "description", "payment_hash", "status", "expires_at" diff --git a/lightningd/invoice.c b/lightningd/invoice.c index fadbb4c6f..1c95e48d4 100644 --- a/lightningd/invoice.c +++ b/lightningd/invoice.c @@ -1382,15 +1382,17 @@ static struct command_result *json_delinvoice(struct command *cmd, const jsmntok_t *params) { struct invoice i; - const struct invoice_details *details; + struct invoice_details *details; struct json_stream *response; const char *status, *actual_status; struct json_escape *label; struct wallet *wallet = cmd->ld->wallet; + bool *deldesc; if (!param(cmd, buffer, params, p_req("label", param_label, &label), p_req("status", param_string, &status), + p_opt_def("desconly", param_bool, &deldesc, false), NULL)) return command_param_failed(); @@ -1415,12 +1417,27 @@ static struct command_result *json_delinvoice(struct command *cmd, return command_failed(cmd, js); } - if (!wallet_invoice_delete(wallet, i)) { - log_broken(cmd->ld->log, - "Error attempting to remove invoice %"PRIu64, - i.id); - /* FIXME: allocate a generic DATABASE_ERROR code. */ - return command_fail(cmd, LIGHTNINGD, "Database error"); + if (*deldesc) { + if (!details->description) + return command_fail(cmd, INVOICE_NO_DESCRIPTION, + "Invoice description already removed"); + + if (!wallet_invoice_delete_description(wallet, i)) { + log_broken(cmd->ld->log, + "Error attempting to delete description of invoice %"PRIu64, + i.id); + /* FIXME: allocate a generic DATABASE_ERROR code. */ + return command_fail(cmd, LIGHTNINGD, "Database error"); + } + details->description = tal_free(details->description); + } else { + if (!wallet_invoice_delete(wallet, i)) { + log_broken(cmd->ld->log, + "Error attempting to remove invoice %"PRIu64, + i.id); + /* FIXME: allocate a generic DATABASE_ERROR code. */ + return command_fail(cmd, LIGHTNINGD, "Database error"); + } } response = json_stream_success(cmd); diff --git a/lightningd/test/run-invoice-select-inchan.c b/lightningd/test/run-invoice-select-inchan.c index ba9193ef0..976af4bb8 100644 --- a/lightningd/test/run-invoice-select-inchan.c +++ b/lightningd/test/run-invoice-select-inchan.c @@ -753,14 +753,18 @@ bool wallet_invoice_create(struct wallet *wallet UNNEEDED, bool wallet_invoice_delete(struct wallet *wallet UNNEEDED, struct invoice invoice UNNEEDED) { fprintf(stderr, "wallet_invoice_delete called!\n"); abort(); } +/* Generated stub for wallet_invoice_delete_description */ +bool wallet_invoice_delete_description(struct wallet *wallet UNNEEDED, + struct invoice invoice UNNEEDED) +{ fprintf(stderr, "wallet_invoice_delete_description called!\n"); abort(); } /* Generated stub for wallet_invoice_delete_expired */ void wallet_invoice_delete_expired(struct wallet *wallet UNNEEDED, u64 max_expiry_time UNNEEDED) { fprintf(stderr, "wallet_invoice_delete_expired called!\n"); abort(); } /* Generated stub for wallet_invoice_details */ -const struct invoice_details *wallet_invoice_details(const tal_t *ctx UNNEEDED, - struct wallet *wallet UNNEEDED, - struct invoice invoice UNNEEDED) +struct invoice_details *wallet_invoice_details(const tal_t *ctx UNNEEDED, + struct wallet *wallet UNNEEDED, + struct invoice invoice UNNEEDED) { fprintf(stderr, "wallet_invoice_details called!\n"); abort(); } /* Generated stub for wallet_invoice_find_by_label */ bool wallet_invoice_find_by_label(struct wallet *wallet UNNEEDED, diff --git a/tests/test_invoices.py b/tests/test_invoices.py index 6cbf960d6..e02dd2561 100644 --- a/tests/test_invoices.py +++ b/tests/test_invoices.py @@ -733,3 +733,10 @@ def test_invoice_deschash(node_factory, chainparams): # Make sure we can pay it! l1.rpc.pay(inv['bolt11']) + + # Try removing description. + l2.rpc.delinvoice('label', "paid", desconly=True) + assert 'description' not in only_one(l2.rpc.listinvoices()['invoices']) + + with pytest.raises(RpcError, match=r'description already removed'): + l2.rpc.delinvoice('label', "paid", desconly=True) diff --git a/wallet/invoices.c b/wallet/invoices.c index f8d46d8d6..2727429a3 100644 --- a/wallet/invoices.c +++ b/wallet/invoices.c @@ -421,6 +421,23 @@ bool invoices_delete(struct invoices *invoices, struct invoice invoice) return true; } +bool invoices_delete_description(struct invoices *invoices, struct invoice invoice) +{ + struct db_stmt *stmt; + int changes; + + stmt = db_prepare_v2(invoices->db, SQL("UPDATE invoices" + " SET description = NULL" + " WHERE ID = ?;")); + db_bind_u64(stmt, 0, invoice.id); + db_exec_prepared_v2(stmt); + + changes = db_count_changes(stmt); + tal_free(stmt); + + return changes == 1; +} + void invoices_delete_expired(struct invoices *invoices, u64 max_expiry_time) { @@ -648,9 +665,9 @@ void invoices_waitone(const tal_t *ctx, false, invoice.id, cb, cbarg); } -const struct invoice_details *invoices_get_details(const tal_t *ctx, - struct invoices *invoices, - struct invoice invoice) +struct invoice_details *invoices_get_details(const tal_t *ctx, + struct invoices *invoices, + struct invoice invoice) { struct db_stmt *stmt; bool res; diff --git a/wallet/invoices.h b/wallet/invoices.h index 00acf384c..7aa584452 100644 --- a/wallet/invoices.h +++ b/wallet/invoices.h @@ -108,6 +108,17 @@ bool invoices_find_unpaid(struct invoices *invoices, bool invoices_delete(struct invoices *invoices, struct invoice invoice); +/** + * invoices_delete_description - Remove description from an invoice + * + * @invoices - the invoice handler. + * @invoice - the invoice to remove description from. + * + * Return false on failure. + */ +bool invoices_delete_description(struct invoices *invoices, + struct invoice invoice); + /** * invoices_delete_expired - Delete all expired invoices * with expiration time less than or equal to the given. @@ -213,8 +224,8 @@ void invoices_waitone(const tal_t *ctx, * @invoice - the invoice to get details on. * @return pointer to the invoice details allocated off of `ctx`. */ -const struct invoice_details *invoices_get_details(const tal_t *ctx, - struct invoices *invoices, - struct invoice invoice); +struct invoice_details *invoices_get_details(const tal_t *ctx, + struct invoices *invoices, + struct invoice invoice); #endif /* LIGHTNING_WALLET_INVOICES_H */ diff --git a/wallet/test/run-wallet.c b/wallet/test/run-wallet.c index 1bbdaf84f..f095d170c 100644 --- a/wallet/test/run-wallet.c +++ b/wallet/test/run-wallet.c @@ -212,6 +212,10 @@ bool invoices_create(struct invoices *invoices UNNEEDED, bool invoices_delete(struct invoices *invoices UNNEEDED, struct invoice invoice UNNEEDED) { fprintf(stderr, "invoices_delete called!\n"); abort(); } +/* Generated stub for invoices_delete_description */ +bool invoices_delete_description(struct invoices *invoices UNNEEDED, + struct invoice invoice UNNEEDED) +{ fprintf(stderr, "invoices_delete_description called!\n"); abort(); } /* Generated stub for invoices_delete_expired */ void invoices_delete_expired(struct invoices *invoices UNNEEDED, u64 max_expiry_time UNNEEDED) @@ -232,9 +236,9 @@ bool invoices_find_unpaid(struct invoices *invoices UNNEEDED, const struct sha256 *rhash UNNEEDED) { fprintf(stderr, "invoices_find_unpaid called!\n"); abort(); } /* Generated stub for invoices_get_details */ -const struct invoice_details *invoices_get_details(const tal_t *ctx UNNEEDED, - struct invoices *invoices UNNEEDED, - struct invoice invoice UNNEEDED) +struct invoice_details *invoices_get_details(const tal_t *ctx UNNEEDED, + struct invoices *invoices UNNEEDED, + struct invoice invoice UNNEEDED) { fprintf(stderr, "invoices_get_details called!\n"); abort(); } /* Generated stub for invoices_iterate */ bool invoices_iterate(struct invoices *invoices UNNEEDED, diff --git a/wallet/wallet.c b/wallet/wallet.c index 47e43bfab..8a71c4113 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -2850,6 +2850,11 @@ bool wallet_invoice_delete(struct wallet *wallet, { return invoices_delete(wallet->invoices, invoice); } +bool wallet_invoice_delete_description(struct wallet *wallet, + struct invoice invoice) +{ + return invoices_delete_description(wallet->invoices, invoice); +} void wallet_invoice_delete_expired(struct wallet *wallet, u64 e) { invoices_delete_expired(wallet->invoices, e); @@ -2888,9 +2893,9 @@ void wallet_invoice_waitone(const tal_t *ctx, invoices_waitone(ctx, wallet->invoices, invoice, cb, cbarg); } -const struct invoice_details *wallet_invoice_details(const tal_t *ctx, - struct wallet *wallet, - struct invoice invoice) +struct invoice_details *wallet_invoice_details(const tal_t *ctx, + struct wallet *wallet, + struct invoice invoice) { return invoices_get_details(ctx, wallet->invoices, invoice); } diff --git a/wallet/wallet.h b/wallet/wallet.h index ad73893ca..6eea26b67 100644 --- a/wallet/wallet.h +++ b/wallet/wallet.h @@ -893,6 +893,9 @@ bool wallet_invoice_find_unpaid(struct wallet *wallet, bool wallet_invoice_delete(struct wallet *wallet, struct invoice invoice); +bool wallet_invoice_delete_description(struct wallet *wallet, + struct invoice invoice); + /** * wallet_invoice_delete_expired - Delete all expired invoices * with expiration time less than or equal to the given. @@ -999,9 +1002,9 @@ void wallet_invoice_waitone(const tal_t *ctx, * @invoice - the invoice to get details on. * @return pointer to the invoice details allocated off of `ctx`. */ -const struct invoice_details *wallet_invoice_details(const tal_t *ctx, - struct wallet *wallet, - struct invoice invoice); +struct invoice_details *wallet_invoice_details(const tal_t *ctx, + struct wallet *wallet, + struct invoice invoice); /** * wallet_htlc_stubs - Retrieve HTLC stubs for the given channel