diff --git a/plugins/.gitignore b/plugins/.gitignore index 7d87bb049..149755c9d 100644 --- a/plugins/.gitignore +++ b/plugins/.gitignore @@ -1,5 +1,6 @@ autoclean bcli +bookkeeper fetchinvoice fundchannel funder diff --git a/plugins/Makefile b/plugins/Makefile index e0236e58e..c9c845519 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -1,3 +1,7 @@ +PLUGIN_BOOKKEEPER_SRC := plugins/bookkeeper.c +PLUGIN_BOOKKEEPER_HEADER := +PLUGIN_BOOKKEEPER_OBJS := $(PLUGIN_BOOKKEEPER_SRC:.c=.o) + PLUGIN_PAY_SRC := plugins/pay.c PLUGIN_PAY_OBJS := $(PLUGIN_PAY_SRC:.c=.o) @@ -60,6 +64,7 @@ PLUGIN_FUNDER_HEADER := \ PLUGIN_FUNDER_OBJS := $(PLUGIN_FUNDER_SRC:.c=.o) PLUGIN_ALL_SRC := \ + $(PLUGIN_BOOKKEEPER_SRC) \ $(PLUGIN_AUTOCLEAN_SRC) \ $(PLUGIN_chanbackup_SRC) \ $(PLUGIN_BCLI_SRC) \ @@ -76,6 +81,7 @@ PLUGIN_ALL_SRC := \ $(PLUGIN_SPENDER_SRC) PLUGIN_ALL_HEADER := \ + $(PLUGIN_BOOKKEEPER_HEADER) \ $(PLUGIN_LIB_HEADER) \ $(PLUGIN_FUNDER_HEADER) \ $(PLUGIN_PAY_LIB_HEADER) \ @@ -87,6 +93,7 @@ C_PLUGINS := \ plugins/autoclean \ plugins/chanbackup \ plugins/bcli \ + plugins/bookkeeper \ plugins/commando \ plugins/fetchinvoice \ plugins/funder \ @@ -166,6 +173,8 @@ PLUGIN_COMMON_OBJS := \ # Make all plugins depend on all plugin headers, for simplicity. $(PLUGIN_ALL_OBJS): $(PLUGIN_ALL_HEADER) +plugins/bookkeeper: bitcoin/chainparams.o common/coin_mvt.o $(PLUGIN_BOOKKEEPER_OBJS) $(PLUGIN_LIB_OBJS) $(JSMN_OBJTS) $(PLUGIN_COMMON_OBJS) $(WIRE_OBJS) + plugins/pay: bitcoin/chainparams.o $(PLUGIN_PAY_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_PAY_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) common/gossmap.o common/fp16.o common/route.o common/dijkstra.o common/bolt12.o common/bolt12_merkle.o wire/bolt12$(EXP)_wiregen.o bitcoin/block.o plugins/autoclean: bitcoin/chainparams.o $(PLUGIN_AUTOCLEAN_OBJS) $(PLUGIN_LIB_OBJS) $(PLUGIN_COMMON_OBJS) $(JSMN_OBJS) diff --git a/plugins/bookkeeper.c b/plugins/bookkeeper.c new file mode 100644 index 000000000..5326f0e5d --- /dev/null +++ b/plugins/bookkeeper.c @@ -0,0 +1,339 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include + +#define CHAIN_MOVE "chain_mvt" +#define CHANNEL_MOVE "channel_mvt" + +static struct command_result *json_list_balances(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + struct json_stream *res; + + if (!param(cmd, buf, params, NULL)) + return command_param_failed(); + + res = jsonrpc_stream_success(cmd); + return command_finished(cmd, res); +} + +struct account_snap { + char *name; + struct amount_msat amt; + char *coin_type; +}; + +static struct command_result *json_balance_snapshot(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + const char *err; + size_t i; + struct node_id node_id; + u32 blockheight; + u64 timestamp; + struct account_snap *snaps; + const jsmntok_t *accounts_tok, *acct_tok, + *snap_tok = json_get_member(buf, params, "balance_snapshot"); + + if (snap_tok == NULL || snap_tok->type != JSMN_OBJECT) + plugin_err(cmd->plugin, + "`balance_snapshot` payload did not scan %s: %.*s", + "no 'balance_snapshot'", json_tok_full_len(params), + json_tok_full(buf, params)); + + err = json_scan(cmd, buf, snap_tok, + "{node_id:%" + ",blockheight:%" + ",timestamp:%}", + JSON_SCAN(json_to_node_id, &node_id), + JSON_SCAN(json_to_number, &blockheight), + JSON_SCAN(json_to_u64, ×tamp)); + + if (err) + plugin_err(cmd->plugin, + "`balance_snapshot` payload did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + plugin_log(cmd->plugin, LOG_DBG, "balances for node %s at %d" + " (%"PRIu64")", + type_to_string(tmpctx, struct node_id, &node_id), + blockheight, timestamp); + + accounts_tok = json_get_member(buf, snap_tok, "accounts"); + if (accounts_tok == NULL || accounts_tok->type != JSMN_ARRAY) + plugin_err(cmd->plugin, + "`balance_snapshot` payload did not scan %s: %.*s", + "no 'balance_snapshot.accounts'", + json_tok_full_len(params), + json_tok_full(buf, params)); + + snaps = tal_arr(cmd, struct account_snap, accounts_tok->size); + json_for_each_arr(i, acct_tok, accounts_tok) { + struct account_snap s = snaps[i]; + err = json_scan(cmd, buf, acct_tok, + "{account_id:%" + ",balance_msat:%" + ",coin_type:%}", + JSON_SCAN_TAL(tmpctx, json_strdup, &s.name), + JSON_SCAN(json_to_msat, &s.amt), + JSON_SCAN_TAL(tmpctx, json_strdup, + &s.coin_type)); + if (err) + plugin_err(cmd->plugin, + "`balance_snapshot` payload did not" + " scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + plugin_log(cmd->plugin, LOG_DBG, "account %s has balance %s", + s.name, + type_to_string(tmpctx, struct amount_msat, &s.amt)); + } + + // FIXME: check balances are ok! + + return notification_handled(cmd); +} + +static const char *parse_and_log_chain_move(struct command *cmd, + const char *buf, + const jsmntok_t *params, + const struct node_id *node_id, + const char *acct_name STEALS, + const struct amount_msat credit, + const struct amount_msat debit, + const char *coin_type STEALS, + const u32 timestamp, + const enum mvt_tag *tags) +{ + struct bitcoin_outpoint outpt; + static struct amount_msat output_value; + struct sha256 *payment_hash = tal(tmpctx, struct sha256); + struct bitcoin_txid *spending_txid = tal(tmpctx, struct bitcoin_txid); + u32 blockheight; + const char *err; + + /* Fields we expect on *every* chain movement */ + err = json_scan(tmpctx, buf, params, + "{coin_movement:" + "{utxo_txid:%" + ",vout:%" + ",output_msat:%" + ",blockheight:%" + "}}", + JSON_SCAN(json_to_txid, &outpt.txid), + JSON_SCAN(json_to_number, &outpt.n), + JSON_SCAN(json_to_msat, &output_value), + JSON_SCAN(json_to_number, &blockheight)); + + if (err) + return err; + + /* Now try to get out the optional parts */ + err = json_scan(tmpctx, buf, params, + "{coin_movement:" + "{txid:%" + "}}", + JSON_SCAN(json_to_txid, spending_txid)); + + if (err) + spending_txid = tal_free(spending_txid); + + /* Now try to get out the optional parts */ + err = json_scan(tmpctx, buf, params, + "{coin_movement:" + "{payment_hash:%" + "}}", + JSON_SCAN(json_to_sha256, payment_hash)); + + if (err) + payment_hash = tal_free(payment_hash); + + // FIXME: enter into database + return NULL; +} + +static const char *parse_and_log_channel_move(struct command *cmd, + const char *buf, + const jsmntok_t *params, + const struct node_id *node_id, + const char *acct_name STEALS, + const struct amount_msat credit, + const struct amount_msat debit, + const char *coin_type STEALS, + const u32 timestamp, + const enum mvt_tag *tags) +{ + struct sha256 payment_hash; + u64 part_id; + struct amount_msat fees; + + const char *err; + + err = json_scan(tmpctx, buf, params, + "{coin_movement:" + "{payment_hash:%" + ",fees:%" + "}}", + JSON_SCAN(json_to_sha256, &payment_hash), + JSON_SCAN(json_to_msat, &fees)); + + if (err) + return err; + + err = json_scan(tmpctx, buf, params, + "{coin_movement:" + "{part_id:%}}", + JSON_SCAN(json_to_u64, &part_id)); + if (err) + part_id = 0; + + // FIXME: enter into database? + + return NULL; +} + +static char *parse_tags(const tal_t *ctx, + const char *buf, + const jsmntok_t *tok, + enum mvt_tag **tags) +{ + size_t i; + const jsmntok_t *tag_tok, + *tags_tok = json_get_member(buf, tok, "tags"); + + if (tags_tok == NULL || tags_tok->type != JSMN_ARRAY) + return "Invalid/missing 'tags' field"; + + *tags = tal_arr(ctx, enum mvt_tag, tags_tok->size); + json_for_each_arr(i, tag_tok, tags_tok) { + if (!json_to_coin_mvt_tag(buf, tag_tok, &(*tags)[i])) + return "Unable to parse 'tags'"; + } + + return NULL; +} + +static struct command_result * json_coin_moved(struct command *cmd, + const char *buf, + const jsmntok_t *params) +{ + const char *err, *mvt_type, *acct_name, *coin_type; + struct node_id node_id; + u32 version; + u64 timestamp; + struct amount_msat credit, debit; + enum mvt_tag *tags; + + err = json_scan(tmpctx, buf, params, + "{coin_movement:" + "{version:%" + ",node_id:%" + ",type:%" + ",account_id:%" + ",credit_msat:%" + ",debit_msat:%" + ",coin_type:%" + ",timestamp:%" + "}}", + JSON_SCAN(json_to_number, &version), + JSON_SCAN(json_to_node_id, &node_id), + JSON_SCAN_TAL(tmpctx, json_strdup, &mvt_type), + JSON_SCAN_TAL(tmpctx, json_strdup, &acct_name), + JSON_SCAN(json_to_msat, &credit), + JSON_SCAN(json_to_msat, &debit), + JSON_SCAN_TAL(tmpctx, json_strdup, &coin_type), + JSON_SCAN(json_to_u64, ×tamp)); + + if (err) + plugin_err(cmd->plugin, + "`coin_movement` payload did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + err = parse_tags(cmd, buf, + json_get_member(buf, params, "coin_movement"), + &tags); + if (err) + plugin_err(cmd->plugin, + "`coin_movement` payload did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + /* We expect version 2 of coin movements */ + assert(version == 2); + + plugin_log(cmd->plugin, LOG_DBG, "coin_move %d %s -%s %s %"PRIu64, + version, + type_to_string(tmpctx, struct amount_msat, &credit), + type_to_string(tmpctx, struct amount_msat, &debit), + mvt_type, timestamp); + + if (streq(mvt_type, CHAIN_MOVE)) + err = parse_and_log_chain_move(cmd, buf, params, &node_id, + acct_name, credit, debit, + coin_type, timestamp, + tags); + else { + assert(streq(mvt_type, CHANNEL_MOVE)); + err = parse_and_log_channel_move(cmd, buf, params, &node_id, + acct_name, credit, debit, + coin_type, timestamp, + tags); + } + + if (err) + plugin_err(cmd->plugin, + "`coin_movement` payload did not scan %s: %.*s", + err, json_tok_full_len(params), + json_tok_full(buf, params)); + + return notification_handled(cmd); +} + +const struct plugin_notification notifs[] = { + { + "coin_movement", + json_coin_moved, + }, + { + "balance_snapshot", + json_balance_snapshot, + } +}; + +static const struct plugin_command commands[] = { + { + "listbalances", + "bookkeeping", + "List current account balances", + "List of current accounts and their balances", + json_list_balances + }, +}; + +static const char *init(struct plugin *p, const char *b, const jsmntok_t *t) +{ + // FIXME set up database, run migrations + return NULL; +} + +int main(int argc, char *argv[]) +{ + setup_locale(); + + plugin_main(argv, init, PLUGIN_STATIC, true, NULL, + commands, ARRAY_SIZE(commands), + notifs, ARRAY_SIZE(notifs), + NULL, 0, + NULL, 0, + NULL); + return 0; +}