core-lightning/wallet/invoices.c
Rusty Russell aad4495f56 delinvoice: allow desconly arg to only remove the description.
Means that field is now optional in JSON output.

Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
Changelog-Added: JSON-RPC: `delinvoice` has a new parameter `desconly` to remove description.
2022-03-29 10:04:16 +10:30

701 lines
17 KiB
C

#include "config.h"
#include <ccan/tal/str/str.h>
#include <common/timeout.h>
#include <db/bindings.h>
#include <db/common.h>
#include <db/exec.h>
#include <db/utils.h>
#include <wallet/invoices.h>
#include <wallet/wallet.h>
struct invoice_waiter {
/* Is this waiter already triggered? */
bool triggered;
/* Is this waiting for any invoice to resolve? */
bool any;
/* If !any, the specific invoice this is waiting on */
u64 id;
struct list_node list;
/* The callback to use */
void (*cb)(const struct invoice *, void*);
void *cbarg;
};
struct invoices {
/* The database connection to use. */
struct db *db;
/* The timers object to use for expirations. */
struct timers *timers;
/* Waiters waiting for invoices to be paid, expired, or deleted. */
struct list_head waiters;
/* Earliest time for some invoice to expire */
u64 min_expiry_time;
/* Expiration timer */
struct oneshot *expiration_timer;
};
static void trigger_invoice_waiter(struct invoice_waiter *w,
const struct invoice *invoice)
{
w->triggered = true;
w->cb(invoice, w->cbarg);
}
static void trigger_invoice_waiter_resolve(struct invoices *invoices,
u64 id,
const struct invoice *invoice)
{
struct invoice_waiter *w;
struct invoice_waiter *n;
list_for_each_safe(&invoices->waiters, w, n, list) {
if (!w->any && w->id != id)
continue;
list_del_from(&invoices->waiters, &w->list);
tal_steal(tmpctx, w);
trigger_invoice_waiter(w, invoice);
}
}
static void
trigger_invoice_waiter_expire_or_delete(struct invoices *invoices,
u64 id,
const struct invoice *invoice)
{
struct invoice_waiter *w;
struct invoice_waiter *n;
list_for_each_safe(&invoices->waiters, w, n, list) {
if (w->any || w->id != id)
continue;
list_del_from(&invoices->waiters, &w->list);
tal_steal(tmpctx, w);
trigger_invoice_waiter(w, invoice);
}
}
static struct invoice_details *wallet_stmt2invoice_details(const tal_t *ctx,
struct db_stmt *stmt)
{
struct invoice_details *dtl = tal(ctx, struct invoice_details);
dtl->state = db_col_int(stmt, "state");
db_col_preimage(stmt, "payment_key", &dtl->r);
db_col_sha256(stmt, "payment_hash", &dtl->rhash);
dtl->label = db_col_json_escape(dtl, stmt, "label");
if (!db_col_is_null(stmt, "msatoshi")) {
dtl->msat = tal(dtl, struct amount_msat);
db_col_amount_msat(stmt, "msatoshi", dtl->msat);
} else {
dtl->msat = NULL;
}
dtl->expiry_time = db_col_u64(stmt, "expiry_time");
if (dtl->state == PAID) {
dtl->pay_index = db_col_u64(stmt, "pay_index");
db_col_amount_msat(stmt, "msatoshi_received", &dtl->received);
dtl->paid_timestamp = db_col_u64(stmt, "paid_timestamp");
} else {
db_col_ignore(stmt, "pay_index");
db_col_ignore(stmt, "msatoshi_received");
db_col_ignore(stmt, "paid_timestamp");
}
dtl->invstring = db_col_strdup(dtl, stmt, "bolt11");
if (!db_col_is_null(stmt, "description"))
dtl->description = db_col_strdup(dtl, stmt,
"description");
else
dtl->description = NULL;
dtl->features = db_col_arr(dtl, stmt, "features", u8);
if (!db_col_is_null(stmt, "local_offer_id")) {
dtl->local_offer_id = tal(dtl, struct sha256);
db_col_sha256(stmt, "local_offer_id",
dtl->local_offer_id);
} else
dtl->local_offer_id = NULL;
return dtl;
}
/* Update expirations. */
static void update_db_expirations(struct invoices *invoices, u64 now)
{
struct db_stmt *stmt;
stmt = db_prepare_v2(invoices->db, SQL("UPDATE invoices"
" SET state = ?"
" WHERE state = ?"
" AND expiry_time <= ?;"));
db_bind_int(stmt, 0, EXPIRED);
db_bind_int(stmt, 1, UNPAID);
db_bind_u64(stmt, 2, now);
db_exec_prepared_v2(take(stmt));
}
static void install_expiration_timer(struct invoices *invoices);
struct invoices *invoices_new(const tal_t *ctx,
struct db *db,
struct timers *timers)
{
struct invoices *invs = tal(ctx, struct invoices);
invs->db = db;
invs->timers = timers;
list_head_init(&invs->waiters);
invs->expiration_timer = NULL;
update_db_expirations(invs, time_now().ts.tv_sec);
install_expiration_timer(invs);
return invs;
}
struct invoice_id_node {
struct list_node list;
u64 id;
};
static void trigger_expiration(struct invoices *invoices)
{
struct list_head idlist;
struct invoice_id_node *idn;
u64 now = time_now().ts.tv_sec;
struct db_stmt *stmt;
struct invoice i;
/* Free current expiration timer */
invoices->expiration_timer = tal_free(invoices->expiration_timer);
/* Acquire all expired invoices and save them in a list */
list_head_init(&idlist);
stmt = db_prepare_v2(invoices->db, SQL("SELECT id"
" FROM invoices"
" WHERE state = ?"
" AND expiry_time <= ?"));
db_bind_int(stmt, 0, UNPAID);
db_bind_u64(stmt, 1, now);
db_query_prepared(stmt);
while (db_step(stmt)) {
idn = tal(tmpctx, struct invoice_id_node);
list_add_tail(&idlist, &idn->list);
idn->id = db_col_u64(stmt, "id");
}
tal_free(stmt);
/* Expire all those invoices */
update_db_expirations(invoices, now);
/* Trigger expirations */
list_for_each(&idlist, idn, list) {
/* Trigger expiration */
i.id = idn->id;
trigger_invoice_waiter_expire_or_delete(invoices, idn->id, &i);
}
install_expiration_timer(invoices);
}
static void install_expiration_timer(struct invoices *invoices)
{
bool res;
struct db_stmt *stmt;
struct timerel rel;
struct timeabs expiry;
struct timeabs now = time_now();
assert(!invoices->expiration_timer);
/* Find unpaid invoice with nearest expiry time */
stmt = db_prepare_v2(invoices->db, SQL("SELECT MIN(expiry_time)"
" FROM invoices"
" WHERE state = ?;"));
db_bind_int(stmt, 0, UNPAID);
db_query_prepared(stmt);
res = db_step(stmt);
assert(res);
if (db_col_is_null(stmt, "MIN(expiry_time)"))
/* Nothing to install */
goto done;
invoices->min_expiry_time = db_col_u64(stmt,
"MIN(expiry_time)");
memset(&expiry, 0, sizeof(expiry));
expiry.ts.tv_sec = invoices->min_expiry_time;
/* now > expiry */
if (time_after(now, expiry))
expiry = now;
/* rel = expiry - now */
rel = time_between(expiry, now);
/* Have it called at indicated timerel. */
invoices->expiration_timer = new_reltimer(invoices->timers,
invoices,
rel,
&trigger_expiration,
invoices);
done:
tal_free(stmt);
}
bool invoices_create(struct invoices *invoices,
struct invoice *pinvoice,
const struct amount_msat *msat TAKES,
const struct json_escape *label TAKES,
u64 expiry,
const char *b11enc,
const char *description,
const u8 *features,
const struct preimage *r,
const struct sha256 *rhash,
const struct sha256 *local_offer_id)
{
struct db_stmt *stmt;
struct invoice dummy;
u64 expiry_time;
u64 now = time_now().ts.tv_sec;
if (invoices_find_by_label(invoices, &dummy, label)) {
if (taken(msat))
tal_free(msat);
if (taken(label))
tal_free(label);
return false;
}
/* Compute expiration. */
expiry_time = now + expiry;
/* Save to database. */
stmt = db_prepare_v2(
invoices->db,
SQL("INSERT INTO invoices"
" ( payment_hash, payment_key, state"
" , msatoshi, label, expiry_time"
" , pay_index, msatoshi_received"
" , paid_timestamp, bolt11, description, features, local_offer_id)"
" VALUES ( ?, ?, ?"
" , ?, ?, ?"
" , NULL, NULL"
" , NULL, ?, ?, ?, ?);"));
db_bind_sha256(stmt, 0, rhash);
db_bind_preimage(stmt, 1, r);
db_bind_int(stmt, 2, UNPAID);
if (msat)
db_bind_amount_msat(stmt, 3, msat);
else
db_bind_null(stmt, 3);
db_bind_json_escape(stmt, 4, label);
db_bind_u64(stmt, 5, expiry_time);
db_bind_text(stmt, 6, b11enc);
if (!description)
db_bind_null(stmt, 7);
else
db_bind_text(stmt, 7, description);
db_bind_talarr(stmt, 8, features);
if (local_offer_id)
db_bind_sha256(stmt, 9, local_offer_id);
else
db_bind_null(stmt, 9);
db_exec_prepared_v2(stmt);
pinvoice->id = db_last_insert_id_v2(take(stmt));
/* Install expiration trigger. */
if (!invoices->expiration_timer ||
expiry_time < invoices->min_expiry_time) {
invoices->expiration_timer
= tal_free(invoices->expiration_timer);
install_expiration_timer(invoices);
}
if (taken(msat))
tal_free(msat);
if (taken(label))
tal_free(label);
return true;
}
bool invoices_find_by_label(struct invoices *invoices,
struct invoice *pinvoice,
const struct json_escape *label)
{
struct db_stmt *stmt;
stmt = db_prepare_v2(invoices->db, SQL("SELECT id"
" FROM invoices"
" WHERE label = ?;"));
db_bind_json_escape(stmt, 0, label);
db_query_prepared(stmt);
if (!db_step(stmt)) {
tal_free(stmt);
return false;
}
pinvoice->id = db_col_u64(stmt, "id");
tal_free(stmt);
return true;
}
bool invoices_find_by_rhash(struct invoices *invoices,
struct invoice *pinvoice,
const struct sha256 *rhash)
{
struct db_stmt *stmt;
stmt = db_prepare_v2(invoices->db, SQL("SELECT id"
" FROM invoices"
" WHERE payment_hash = ?;"));
db_bind_sha256(stmt, 0, rhash);
db_query_prepared(stmt);
if (!db_step(stmt)) {
tal_free(stmt);
return false;
} else {
pinvoice->id = db_col_u64(stmt, "id");
tal_free(stmt);
return true;
}
}
bool invoices_find_unpaid(struct invoices *invoices,
struct invoice *pinvoice,
const struct sha256 *rhash)
{
struct db_stmt *stmt;
stmt = db_prepare_v2(invoices->db, SQL("SELECT id"
" FROM invoices"
" WHERE payment_hash = ?"
" AND state = ?;"));
db_bind_sha256(stmt, 0, rhash);
db_bind_int(stmt, 1, UNPAID);
db_query_prepared(stmt);
if (!db_step(stmt)) {
tal_free(stmt);
return false;
} else {
pinvoice->id = db_col_u64(stmt, "id");
tal_free(stmt);
return true;
}
}
bool invoices_delete(struct invoices *invoices, struct invoice invoice)
{
struct db_stmt *stmt;
int changes;
/* Delete from database. */
stmt = db_prepare_v2(invoices->db,
SQL("DELETE FROM invoices WHERE id=?;"));
db_bind_u64(stmt, 0, invoice.id);
db_exec_prepared_v2(stmt);
changes = db_count_changes(stmt);
tal_free(stmt);
if (changes != 1) {
return false;
}
/* Tell all the waiters about the fact that it was deleted. */
trigger_invoice_waiter_expire_or_delete(invoices, invoice.id, NULL);
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)
{
struct db_stmt *stmt;
stmt = db_prepare_v2(invoices->db, SQL(
"DELETE FROM invoices"
" WHERE state = ?"
" AND expiry_time <= ?;"));
db_bind_int(stmt, 0, EXPIRED);
db_bind_u64(stmt, 1, max_expiry_time);
db_exec_prepared_v2(take(stmt));
}
bool invoices_iterate(struct invoices *invoices,
struct invoice_iterator *it)
{
struct db_stmt *stmt;
if (!it->p) {
stmt = db_prepare_v2(invoices->db, SQL("SELECT"
" state"
", payment_key"
", payment_hash"
", label"
", msatoshi"
", expiry_time"
", pay_index"
", msatoshi_received"
", paid_timestamp"
", bolt11"
", description"
", features"
", local_offer_id"
" FROM invoices"
" ORDER BY id;"));
db_query_prepared(stmt);
it->p = stmt;
} else
stmt = it->p;
if (db_step(stmt))
/* stmt doesn't need to be freed since we expect to be called
* again, and stmt will be freed on the last iteration. */
return true;
tal_free(stmt);
it->p = NULL;
return false;
}
const struct invoice_details *
invoices_iterator_deref(const tal_t *ctx, struct invoices *invoices UNUSED,
const struct invoice_iterator *it)
{
assert(it->p);
return wallet_stmt2invoice_details(ctx, (struct db_stmt*) it->p);
}
static s64 get_next_pay_index(struct db *db)
{
/* Equivalent to (next_pay_index++) */
s64 next_pay_index;
next_pay_index = db_get_intvar(db, "next_pay_index", 0);
/* Variable should exist. */
assert(next_pay_index > 0);
db_set_intvar(db, "next_pay_index", next_pay_index + 1);
return next_pay_index;
}
static enum invoice_status invoice_get_status(struct invoices *invoices, struct invoice invoice)
{
struct db_stmt *stmt;
enum invoice_status state;
bool res;
stmt = db_prepare_v2(
invoices->db, SQL("SELECT state FROM invoices WHERE id = ?;"));
db_bind_u64(stmt, 0, invoice.id);
db_query_prepared(stmt);
res = db_step(stmt);
assert(res);
state = db_col_int(stmt, "state");
tal_free(stmt);
return state;
}
/* If there's an associated offer, mark it used. */
static void maybe_mark_offer_used(struct db *db, struct invoice invoice)
{
struct db_stmt *stmt;
struct sha256 local_offer_id;
stmt = db_prepare_v2(
db, SQL("SELECT local_offer_id FROM invoices WHERE id = ?;"));
db_bind_u64(stmt, 0, invoice.id);
db_query_prepared(stmt);
db_step(stmt);
if (db_col_is_null(stmt, "local_offer_id")) {
tal_free(stmt);
return;
}
db_col_sha256(stmt, "local_offer_id", &local_offer_id);
tal_free(stmt);
wallet_offer_mark_used(db, &local_offer_id);
}
bool invoices_resolve(struct invoices *invoices,
struct invoice invoice,
struct amount_msat received)
{
struct db_stmt *stmt;
s64 pay_index;
u64 paid_timestamp;
enum invoice_status state = invoice_get_status(invoices, invoice);
if (state != UNPAID)
return false;
/* Assign a pay-index. */
pay_index = get_next_pay_index(invoices->db);
paid_timestamp = time_now().ts.tv_sec;
/* Update database. */
stmt = db_prepare_v2(invoices->db, SQL("UPDATE invoices"
" SET state=?"
" , pay_index=?"
" , msatoshi_received=?"
" , paid_timestamp=?"
" WHERE id=?;"));
db_bind_int(stmt, 0, PAID);
db_bind_u64(stmt, 1, pay_index);
db_bind_amount_msat(stmt, 2, &received);
db_bind_u64(stmt, 3, paid_timestamp);
db_bind_u64(stmt, 4, invoice.id);
db_exec_prepared_v2(take(stmt));
maybe_mark_offer_used(invoices->db, invoice);
/* Tell all the waiters about the paid invoice. */
trigger_invoice_waiter_resolve(invoices, invoice.id, &invoice);
return true;
}
/* Called when an invoice waiter is destructed. */
static void destroy_invoice_waiter(struct invoice_waiter *w)
{
/* Already triggered. */
if (w->triggered)
return;
list_del(&w->list);
}
/* Add an invoice waiter to the specified list of invoice waiters. */
static void add_invoice_waiter(const tal_t *ctx,
struct list_head *waiters,
bool any,
u64 id,
void (*cb)(const struct invoice *, void*),
void* cbarg)
{
struct invoice_waiter *w = tal(ctx, struct invoice_waiter);
w->triggered = false;
w->any = any;
w->id = id;
list_add_tail(waiters, &w->list);
w->cb = cb;
w->cbarg = cbarg;
tal_add_destructor(w, &destroy_invoice_waiter);
}
void invoices_waitany(const tal_t *ctx,
struct invoices *invoices,
u64 lastpay_index,
void (*cb)(const struct invoice *, void*),
void *cbarg)
{
struct db_stmt *stmt;
struct invoice invoice;
/* Look for an already-paid invoice. */
stmt = db_prepare_v2(invoices->db,
SQL("SELECT id"
" FROM invoices"
" WHERE pay_index IS NOT NULL"
" AND pay_index > ?"
" ORDER BY pay_index ASC LIMIT 1;"));
db_bind_u64(stmt, 0, lastpay_index);
db_query_prepared(stmt);
if (db_step(stmt)) {
invoice.id = db_col_u64(stmt, "id");
cb(&invoice, cbarg);
} else {
/* None found. */
add_invoice_waiter(ctx, &invoices->waiters,
true, 0, cb, cbarg);
}
tal_free(stmt);
}
void invoices_waitone(const tal_t *ctx,
struct invoices *invoices,
struct invoice invoice,
void (*cb)(const struct invoice *, void*),
void *cbarg)
{
enum invoice_status state;
state = invoice_get_status(invoices, invoice);
if (state == PAID || state == EXPIRED) {
cb(&invoice, cbarg);
return;
}
/* Not yet paid. */
add_invoice_waiter(ctx, &invoices->waiters,
false, invoice.id, cb, cbarg);
}
struct invoice_details *invoices_get_details(const tal_t *ctx,
struct invoices *invoices,
struct invoice invoice)
{
struct db_stmt *stmt;
bool res;
struct invoice_details *details;
stmt = db_prepare_v2(invoices->db, SQL("SELECT"
" state"
", payment_key"
", payment_hash"
", label"
", msatoshi"
", expiry_time"
", pay_index"
", msatoshi_received"
", paid_timestamp"
", bolt11"
", description"
", features"
", local_offer_id"
" FROM invoices"
" WHERE id = ?;"));
db_bind_u64(stmt, 0, invoice.id);
db_query_prepared(stmt);
res = db_step(stmt);
assert(res);
details = wallet_stmt2invoice_details(ctx, stmt);
tal_free(stmt);
return details;
}