cln-plugin 0.1.1

A CLN plugin library. Write your plugin in Rust.
Documentation
#include "config.h"
#include <ccan/array_size/array_size.h>
#include <db/bindings.h>
#include <db/common.h>
#include <db/exec.h>
#include <db/utils.h>
#include <plugins/bkpr/db.h>
#include <plugins/libplugin.h>
#include <stdio.h>

struct migration {
	const char *sql;
	void (*func)(struct plugin *p, struct db *db);
};

static struct plugin *plugin;

static void migration_remove_dupe_lease_fees(struct plugin *p, struct db *db);

/* Do not reorder or remove elements from this array.
 * It is used to migrate existing databases from a prevoius state, based on
 * string indices */
static struct migration db_migrations[] = {
	{SQL("CREATE TABLE version (version INTEGER);"), NULL},
	{SQL("INSERT INTO version VALUES (1);"), NULL},
	{SQL("CREATE TABLE vars ("
		"  name TEXT"
		", val TEXT"
		", intval INTEGER"
		", blobval BLOB"
		", PRIMARY KEY (name)"
		");"),
	NULL},
	{SQL("INSERT INTO vars ("
		"  name"
		", intval"
		") VALUES ("
		" 'data_version'"
		", 0"
		");"),
	NULL},
	{SQL("CREATE TABLE accounts ("
		"  id BIGSERIAL"
		", name TEXT"
		", peer_id BLOB"
		", opened_event_id BIGINT"
		", closed_event_id BIGINT"
		", onchain_resolved_block INTEGER"
		", is_wallet INTEGER"
		", we_opened INTEGER"
		", leased INTEGER"
		", PRIMARY KEY (id)"
		");"),
	NULL},
	{SQL("CREATE TABLE chain_events ("
		"  id BIGSERIAL"
		", account_id BIGINT REFERENCES accounts(id)"
		", tag TEXT"
		", credit BIGINT"
		", debit BIGINT"
		", output_value BIGINT"
		", currency TEXT"
		", timestamp BIGINT"
		", blockheight INTEGER"
		", utxo_txid BLOB"
		", outnum INTEGER"
		", payment_id BLOB"
		", spending_txid BLOB"
		", PRIMARY KEY (id)"
		");"),
	NULL},
	{SQL("CREATE TABLE channel_events ("
		"  id BIGSERIAL"
		", account_id BIGINT REFERENCES accounts(id)"
		", tag TEXT"
		", credit BIGINT"
		", debit BIGINT"
		", fees BIGINT"
		", currency TEXT"
		", payment_id BLOB"
		", part_id INTEGER"
		", timestamp BIGINT"
		", PRIMARY KEY (id)"
		");"),
	NULL},
	{SQL("CREATE TABLE onchain_fees ("
		"account_id BIGINT REFERENCES accounts(id)"
		", txid BLOB"
		", credit BIGINT"
		", debit BIGINT"
		", currency TEXT"
		", timestamp BIGINT"
		", update_count INT"
		", PRIMARY KEY (account_id, txid, update_count)"
		");"),
	NULL},
	{SQL("ALTER TABLE chain_events ADD origin TEXT;"), NULL},
	{SQL("ALTER TABLE accounts ADD closed_count INTEGER DEFAULT 0;"), NULL},
	{SQL("ALTER TABLE chain_events ADD ignored INTEGER;"), NULL},
	{SQL("ALTER TABLE chain_events ADD stealable INTEGER;"), NULL},
	{SQL("ALTER TABLE chain_events ADD ev_desc TEXT DEFAULT NULL;"), NULL},
	{SQL("ALTER TABLE channel_events ADD ev_desc TEXT DEFAULT NULL;"), NULL},
	{SQL("ALTER TABLE channel_events ADD rebalance_id BIGINT DEFAULT NULL;"), NULL},
	{NULL, migration_remove_dupe_lease_fees}
};

static bool db_migrate(struct plugin *p, struct db *db, bool *created)
{
	/* Read current version from database */
	int current, orig, available;
	struct db_stmt *stmt;

	orig = current = db_get_version(db);
	available = ARRAY_SIZE(db_migrations) - 1;

	*created = false;
	if (current == -1) {
		*created = true;
		plugin_log(p, LOG_INFORM, "Creating database");
	} else if (available < current)
		plugin_err(p,
			   "Refusing to migrate down from version %u to %u",
			   current, available);
	else if (current != available)
		plugin_log(p, LOG_INFORM,
			   "Updating database from version %u to %u",
			   current, available);

	while (current < available) {
		current++;
		if (db_migrations[current].sql) {
			stmt = db_prepare_v2(db, db_migrations[current].sql);
			db_exec_prepared_v2(take(stmt));
		}
		if (db_migrations[current].func)
			db_migrations[current].func(p, db);
	}

	/* Finally, update the version number in the version table */
	stmt = db_prepare_v2(db, SQL("UPDATE version SET version=?;"));
	db_bind_int(stmt, 0, available);
	db_exec_prepared_v2(take(stmt));

	return current != orig;
}

static void migration_remove_dupe_lease_fees(struct plugin *p, struct db *db)
{
	struct db_stmt *stmt, *del_stmt;
	u64 *last_acct_id;

	stmt = db_prepare_v2(db, SQL("SELECT"
				     "  id"
				     ", account_id"
				     " FROM channel_events"
				     " WHERE tag = 'lease_fee'"
				     " ORDER BY account_id"));
	db_query_prepared(stmt);
	last_acct_id = NULL;
	while (db_step(stmt)) {
		u64 id, acct_id;
		id = db_col_u64(stmt, "id");
		acct_id = db_col_u64(stmt, "account_id");

		if (!last_acct_id) {
			last_acct_id = tal(stmt, u64);
			*last_acct_id = acct_id;
			continue;
		}

		if (*last_acct_id != acct_id) {
			*last_acct_id = acct_id;
			continue;
		}

		plugin_log(plugin, LOG_INFORM,
			   "Duplicate 'lease_fee' found for"
			   " account %"PRIu64", deleting dupe",
			   id);

		/* same acct as last, we found a duplicate */
		del_stmt = db_prepare_v2(db, SQL("DELETE FROM channel_events"
						 " WHERE id=?"));
		db_bind_u64(del_stmt, 0, id);
		db_exec_prepared_v2(take(del_stmt));
	}
	tal_free(stmt);
}

/* Implement db_fatal, as a wrapper around fatal.
 * We use a ifndef block so that it can get be
 * implemented in a test file first, if necessary */
#ifndef DB_FATAL
#define DB_FATAL
void db_fatal(const char *fmt, ...)
{
	va_list ap;
	va_start(ap, fmt);
	plugin_errv(plugin, fmt, ap);
	/* Won't actually exit, but va_end() required to balance va_start in standard. */
	va_end(ap);
}
#endif /* DB_FATAL */

struct db *db_setup(const tal_t *ctx, struct plugin *p,
		    const char *db_dsn, bool *created)
{
	bool migrated;
	struct db *db;

	/* Set global for db_fatal */
	plugin = p;
	db = db_open(ctx, db_dsn);
	db->report_changes_fn = NULL;

	db_begin_transaction(db);
	migrated = db_migrate(p, db, created);
	db->data_version = db_data_version_get(db);
	db_commit_transaction(db);

	/* This needs to be done outside a transaction, apparently.
	 * It's a good idea to do this every so often, and on db
	 * upgrade is a reasonable time. */
	if (migrated && !db->config->vacuum_fn(db))
		db_fatal("Error vacuuming db: %s", db->error);

	return db;
}