mirror of
https://github.com/ElementsProject/lightning.git
synced 2024-11-20 02:27:51 +01:00
5b58eda748
This is what we do in lightningd, which makes memleak much more forgiving: you can hang temporaries off cmd without getting reports of leaks (also when send_outreq called). We remove all the notleak() calls in plugins which worked around this! And avoid multiple notleak labels, since both send_outreq() and command_still_pending() can be called multiple times. Signed-off-by: Rusty Russell <rusty@rustcorp.com.au>
1880 lines
49 KiB
C
1880 lines
49 KiB
C
#include "config.h"
|
|
#include <ccan/array_size/array_size.h>
|
|
#include <ccan/cast/cast.h>
|
|
#include <ccan/json_escape/json_escape.h>
|
|
#include <ccan/tal/str/str.h>
|
|
#include <ccan/tal/tal.h>
|
|
#include <ccan/time/time.h>
|
|
#include <common/bolt11.h>
|
|
#include <common/bolt12.h>
|
|
#include <common/coin_mvt.h>
|
|
#include <common/json_param.h>
|
|
#include <common/json_stream.h>
|
|
#include <common/memleak.h>
|
|
#include <common/node_id.h>
|
|
#include <common/type_to_string.h>
|
|
#include <db/exec.h>
|
|
#include <errno.h>
|
|
#include <plugins/bkpr/account.h>
|
|
#include <plugins/bkpr/account_entry.h>
|
|
#include <plugins/bkpr/chain_event.h>
|
|
#include <plugins/bkpr/channel_event.h>
|
|
#include <plugins/bkpr/channelsapy.h>
|
|
#include <plugins/bkpr/db.h>
|
|
#include <plugins/bkpr/incomestmt.h>
|
|
#include <plugins/bkpr/onchain_fee.h>
|
|
#include <plugins/bkpr/recorder.h>
|
|
#include <plugins/libplugin.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#define CHAIN_MOVE "chain_mvt"
|
|
#define CHANNEL_MOVE "channel_mvt"
|
|
|
|
/* The database that we store all the accounting data in */
|
|
static struct db *db ;
|
|
|
|
static char *db_dsn;
|
|
static char *datadir;
|
|
static bool tom_jones;
|
|
|
|
static struct fee_sum *find_sum_for_txid(struct fee_sum **sums,
|
|
struct bitcoin_txid *txid)
|
|
{
|
|
for (size_t i = 0; i < tal_count(sums); i++) {
|
|
if (bitcoin_txid_eq(txid, sums[i]->txid))
|
|
return sums[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
struct apy_req {
|
|
u64 *start_time;
|
|
u64 *end_time;
|
|
};
|
|
|
|
static struct command_result *
|
|
getblockheight_done(struct command *cmd, const char *buf,
|
|
const jsmntok_t *result,
|
|
struct apy_req *req)
|
|
{
|
|
const jsmntok_t *blockheight_tok;
|
|
u32 blockheight;
|
|
struct json_stream *res;
|
|
struct channel_apy **apys, *net_apys;
|
|
|
|
blockheight_tok = json_get_member(buf, result, "blockheight");
|
|
if (!blockheight_tok)
|
|
plugin_err(cmd->plugin, "getblockheight: "
|
|
"getinfo gave no 'blockheight'? '%.*s'",
|
|
result->end - result->start, buf);
|
|
|
|
if (!json_to_u32(buf, blockheight_tok, &blockheight))
|
|
plugin_err(cmd->plugin, "getblockheight: "
|
|
"getinfo gave non-unsigned-32-bit 'blockheight'? '%.*s'",
|
|
result->end - result->start, buf);
|
|
|
|
/* Get the income events */
|
|
db_begin_transaction(db);
|
|
apys = compute_channel_apys(cmd, db,
|
|
*req->start_time,
|
|
*req->end_time,
|
|
blockheight);
|
|
db_commit_transaction(db);
|
|
|
|
/* Setup the net_apys entry */
|
|
net_apys = new_channel_apy(cmd);
|
|
net_apys->end_blockheight = 0;
|
|
net_apys->start_blockheight = UINT_MAX;
|
|
net_apys->our_start_bal = AMOUNT_MSAT(0);
|
|
net_apys->total_start_bal = AMOUNT_MSAT(0);
|
|
|
|
res = jsonrpc_stream_success(cmd);
|
|
json_array_start(res, "channels_apy");
|
|
for (size_t i = 0; i < tal_count(apys); i++) {
|
|
json_add_channel_apy(res, apys[i]);
|
|
|
|
/* Add to net/rollup APY */
|
|
if (!channel_apy_sum(net_apys, apys[i]))
|
|
return command_fail(cmd, PLUGIN_ERROR,
|
|
"Overflow adding APYs net");
|
|
}
|
|
|
|
/* Append a net/rollup entry */
|
|
if (!amount_msat_zero(net_apys->total_start_bal)) {
|
|
net_apys->acct_name = tal_fmt(net_apys, "net");
|
|
json_add_channel_apy(res, net_apys);
|
|
}
|
|
json_array_end(res);
|
|
|
|
return command_finished(cmd, res);
|
|
}
|
|
|
|
static struct command_result *json_channel_apy(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params)
|
|
{
|
|
struct out_req *req;
|
|
struct apy_req *apyreq = tal(cmd, struct apy_req);
|
|
|
|
if (!param(cmd, buf, params,
|
|
p_opt_def("start_time", param_u64, &apyreq->start_time, 0),
|
|
p_opt_def("end_time", param_u64, &apyreq->end_time,
|
|
SQLITE_MAX_UINT),
|
|
NULL))
|
|
return command_param_failed();
|
|
|
|
/* First get the current blockheight */
|
|
req = jsonrpc_request_start(cmd->plugin, cmd, "getinfo",
|
|
&getblockheight_done,
|
|
forward_error,
|
|
apyreq);
|
|
return send_outreq(cmd->plugin, req);
|
|
}
|
|
|
|
static struct command_result *param_csv_format(struct command *cmd, const char *name,
|
|
const char *buffer, const jsmntok_t *tok,
|
|
struct csv_fmt **csv_fmt)
|
|
{
|
|
*csv_fmt = cast_const(struct csv_fmt *,
|
|
csv_match_token(buffer, tok));
|
|
if (*csv_fmt)
|
|
return NULL;
|
|
|
|
return command_fail_badparam(cmd, name, buffer, tok,
|
|
tal_fmt(cmd,
|
|
"should be one of: %s",
|
|
csv_list_fmts(cmd)));
|
|
}
|
|
|
|
static struct command_result *json_dump_income(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params)
|
|
{
|
|
struct json_stream *res;
|
|
struct income_event **evs;
|
|
struct csv_fmt *csv_fmt;
|
|
const char *filename;
|
|
bool *consolidate_fees;
|
|
char *err;
|
|
u64 *start_time, *end_time;
|
|
|
|
if (!param(cmd, buf, params,
|
|
p_req("csv_format", param_csv_format, &csv_fmt),
|
|
p_opt("csv_file", param_string, &filename),
|
|
p_opt_def("consolidate_fees", param_bool,
|
|
&consolidate_fees, true),
|
|
p_opt_def("start_time", param_u64, &start_time, 0),
|
|
p_opt_def("end_time", param_u64, &end_time, SQLITE_MAX_UINT),
|
|
NULL))
|
|
return command_param_failed();
|
|
|
|
/* Ok, go find me some income events! */
|
|
db_begin_transaction(db);
|
|
evs = list_income_events(cmd, db, *start_time, *end_time,
|
|
*consolidate_fees);
|
|
db_commit_transaction(db);
|
|
|
|
if (!filename)
|
|
filename = csv_filename(cmd, csv_fmt);
|
|
|
|
err = csv_print_income_events(cmd, csv_fmt, filename, evs);
|
|
if (err)
|
|
return command_fail(cmd, PLUGIN_ERROR,
|
|
"Unable to create csv file: %s",
|
|
err);
|
|
|
|
res = jsonrpc_stream_success(cmd);
|
|
json_add_string(res, "csv_file", filename);
|
|
json_add_string(res, "csv_format", csv_fmt->fmt_name);
|
|
return command_finished(cmd, res);
|
|
}
|
|
|
|
static struct command_result *json_list_income(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params)
|
|
{
|
|
struct json_stream *res;
|
|
struct income_event **evs;
|
|
bool *consolidate_fees;
|
|
u64 *start_time, *end_time;
|
|
|
|
if (!param(cmd, buf, params,
|
|
p_opt_def("consolidate_fees", param_bool,
|
|
&consolidate_fees, true),
|
|
p_opt_def("start_time", param_u64, &start_time, 0),
|
|
p_opt_def("end_time", param_u64, &end_time, SQLITE_MAX_UINT),
|
|
NULL))
|
|
return command_param_failed();
|
|
|
|
/* Ok, go find me some income events! */
|
|
db_begin_transaction(db);
|
|
evs = list_income_events(cmd, db, *start_time, *end_time,
|
|
*consolidate_fees);
|
|
db_commit_transaction(db);
|
|
|
|
res = jsonrpc_stream_success(cmd);
|
|
|
|
json_array_start(res, "income_events");
|
|
for (size_t i = 0; i < tal_count(evs); i++)
|
|
json_add_income_event(res, evs[i]);
|
|
|
|
json_array_end(res);
|
|
return command_finished(cmd, res);
|
|
}
|
|
|
|
static struct command_result *json_inspect(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params)
|
|
{
|
|
struct json_stream *res;
|
|
struct account *acct;
|
|
const char *acct_name;
|
|
struct fee_sum **fee_sums;
|
|
struct txo_set **txos;
|
|
|
|
/* Only available for channel accounts? */
|
|
if (!param(cmd, buf, params,
|
|
p_req("account", param_string, &acct_name),
|
|
NULL))
|
|
return command_param_failed();
|
|
|
|
if (streq(acct_name, WALLET_ACCT)
|
|
|| streq(acct_name, EXTERNAL_ACCT))
|
|
return command_fail(cmd, PLUGIN_ERROR,
|
|
"`inspect` not supported for"
|
|
" non-channel accounts");
|
|
|
|
db_begin_transaction(db);
|
|
acct = find_account(cmd, db, acct_name);
|
|
db_commit_transaction(db);
|
|
|
|
if (!acct)
|
|
return command_fail(cmd, PLUGIN_ERROR,
|
|
"Account %s not found",
|
|
acct_name);
|
|
|
|
db_begin_transaction(db);
|
|
find_txo_chain(cmd, db, acct, &txos);
|
|
fee_sums = find_account_onchain_fees(cmd, db, acct);
|
|
db_commit_transaction(db);
|
|
|
|
res = jsonrpc_stream_success(cmd);
|
|
json_array_start(res, "txs");
|
|
for (size_t i = 0; i < tal_count(txos); i++) {
|
|
struct txo_set *set = txos[i];
|
|
struct fee_sum *fee_sum;
|
|
|
|
json_object_start(res, NULL);
|
|
json_add_txid(res, "txid", set->txid);
|
|
|
|
/* annoyting, but we can only add the block height
|
|
* if we have a txo for it */
|
|
for (size_t j = 0; j < tal_count(set->pairs); j++) {
|
|
if (set->pairs[j]->txo
|
|
&& set->pairs[j]->txo->blockheight > 0) {
|
|
json_add_num(res, "blockheight",
|
|
set->pairs[j]->txo->blockheight);
|
|
break;
|
|
}
|
|
}
|
|
|
|
fee_sum = find_sum_for_txid(fee_sums, set->txid);
|
|
if (fee_sum)
|
|
json_add_amount_msat_only(res, "fees_paid_msat",
|
|
fee_sum->fees_paid);
|
|
else
|
|
json_add_amount_msat_only(res, "fees_paid_msat",
|
|
AMOUNT_MSAT(0));
|
|
|
|
json_array_start(res, "outputs");
|
|
for (size_t j = 0; j < tal_count(set->pairs); j++) {
|
|
struct txo_pair *pr = set->pairs[j];
|
|
|
|
/* Is this an event that belongs to this account? */
|
|
if (pr->txo) {
|
|
if (pr->txo->origin_acct) {
|
|
if (!streq(pr->txo->origin_acct, acct->name))
|
|
continue;
|
|
} else if (pr->txo->acct_db_id != acct->db_id
|
|
/* We make an exception for wallet events */
|
|
&& !streq(pr->txo->acct_name, WALLET_ACCT))
|
|
continue;
|
|
} else if (pr->spend
|
|
&& pr->spend->acct_db_id != acct->db_id)
|
|
continue;
|
|
|
|
json_object_start(res, NULL);
|
|
if (set->pairs[j]->txo) {
|
|
struct chain_event *ev = set->pairs[j]->txo;
|
|
|
|
json_add_string(res, "account", ev->acct_name);
|
|
json_add_num(res, "outnum",
|
|
ev->outpoint.n);
|
|
json_add_string(res, "output_tag", ev->tag);
|
|
json_add_amount_msat_only(res, "output_value_msat",
|
|
ev->output_value);
|
|
json_add_amount_msat_only(res, "credit_msat",
|
|
ev->credit);
|
|
json_add_string(res, "currency", ev->currency);
|
|
if (ev->origin_acct)
|
|
json_add_string(res, "originating_account",
|
|
ev->origin_acct);
|
|
}
|
|
if (set->pairs[j]->spend) {
|
|
struct chain_event *ev = set->pairs[j]->spend;
|
|
/* If we didn't already populate this info */
|
|
if (!set->pairs[j]->txo) {
|
|
json_add_string(res, "account",
|
|
ev->acct_name);
|
|
json_add_num(res, "outnum",
|
|
ev->outpoint.n);
|
|
json_add_amount_msat_only(res, "output_value_msat",
|
|
ev->output_value);
|
|
json_add_string(res, "currency",
|
|
ev->currency);
|
|
}
|
|
json_add_string(res, "spend_tag", ev->tag);
|
|
json_add_txid(res, "spending_txid",
|
|
ev->spending_txid);
|
|
json_add_amount_msat_only(res, "debit_msat", ev->debit);
|
|
if (ev->payment_id)
|
|
json_add_sha256(res, "payment_id",
|
|
ev->payment_id);
|
|
}
|
|
json_object_end(res);
|
|
}
|
|
json_array_end(res);
|
|
json_object_end(res);
|
|
}
|
|
json_array_end(res);
|
|
|
|
return command_finished(cmd, res);
|
|
}
|
|
|
|
/* Find all the events for this account, ordered by timestamp */
|
|
static struct command_result *json_list_account_events(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params)
|
|
{
|
|
struct json_stream *res;
|
|
struct account *acct;
|
|
const char *acct_name;
|
|
struct channel_event **channel_events;
|
|
struct chain_event **chain_events;
|
|
struct onchain_fee **onchain_fees;
|
|
|
|
if (!param(cmd, buf, params,
|
|
p_opt("account", param_string, &acct_name),
|
|
NULL))
|
|
return command_param_failed();
|
|
|
|
if (acct_name) {
|
|
db_begin_transaction(db);
|
|
acct = find_account(cmd, db, acct_name);
|
|
db_commit_transaction(db);
|
|
|
|
if (!acct)
|
|
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
|
|
"Account '%s' not found",
|
|
acct_name);
|
|
} else
|
|
acct = NULL;
|
|
|
|
db_begin_transaction(db);
|
|
if (acct) {
|
|
channel_events = account_get_channel_events(cmd, db, acct);
|
|
chain_events = account_get_chain_events(cmd, db, acct);
|
|
onchain_fees = account_get_chain_fees(cmd, db, acct);
|
|
} else {
|
|
channel_events = list_channel_events(cmd, db);
|
|
chain_events = list_chain_events(cmd, db);
|
|
onchain_fees = list_chain_fees(cmd, db);
|
|
}
|
|
db_commit_transaction(db);
|
|
|
|
res = jsonrpc_stream_success(cmd);
|
|
json_array_start(res, "events");
|
|
for (size_t i = 0, j = 0, k = 0;
|
|
i < tal_count(chain_events)
|
|
|| j < tal_count(channel_events)
|
|
|| k < tal_count(onchain_fees);
|
|
/* Incrementing happens inside loop */) {
|
|
struct channel_event *chan;
|
|
struct chain_event *chain;
|
|
struct onchain_fee *fee;
|
|
u64 lowest = 0;
|
|
|
|
if (i < tal_count(chain_events))
|
|
chain = chain_events[i];
|
|
else
|
|
chain = NULL;
|
|
if (j < tal_count(channel_events))
|
|
chan = channel_events[j];
|
|
else
|
|
chan = NULL;
|
|
if (k < tal_count(onchain_fees))
|
|
fee = onchain_fees[k];
|
|
else
|
|
fee = NULL;
|
|
|
|
if (chain)
|
|
lowest = chain->timestamp;
|
|
|
|
if (chan
|
|
&& (lowest == 0 || lowest > chan->timestamp))
|
|
lowest = chan->timestamp;
|
|
|
|
if (fee
|
|
&& (lowest == 0 || lowest > fee->timestamp))
|
|
lowest = fee->timestamp;
|
|
|
|
/* chain events first, then channel events, then fees. */
|
|
if (chain && chain->timestamp == lowest) {
|
|
json_add_chain_event(res, chain);
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
if (chan && chan->timestamp == lowest) {
|
|
json_add_channel_event(res, chan);
|
|
j++;
|
|
continue;
|
|
}
|
|
|
|
/* Last thing left is the fee */
|
|
json_add_onchain_fee(res, fee);
|
|
k++;
|
|
}
|
|
json_array_end(res);
|
|
return command_finished(cmd, res);
|
|
}
|
|
|
|
static struct command_result *json_list_balances(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params)
|
|
{
|
|
struct json_stream *res;
|
|
struct account **accts;
|
|
char *err;
|
|
|
|
if (!param(cmd, buf, params, NULL))
|
|
return command_param_failed();
|
|
|
|
res = jsonrpc_stream_success(cmd);
|
|
/* List of accts */
|
|
db_begin_transaction(db);
|
|
accts = list_accounts(cmd, db);
|
|
|
|
json_array_start(res, "accounts");
|
|
for (size_t i = 0; i < tal_count(accts); i++) {
|
|
struct acct_balance **balances;
|
|
|
|
err = account_get_balance(cmd, db,
|
|
accts[i]->name,
|
|
true,
|
|
false, /* don't skip ignored */
|
|
&balances,
|
|
NULL);
|
|
|
|
if (err)
|
|
plugin_err(cmd->plugin,
|
|
"Get account balance returned err"
|
|
" for account %s: %s",
|
|
accts[i]->name, err);
|
|
|
|
/* Skip the external acct balance, it's effectively
|
|
* meaningless */
|
|
if (streq(accts[i]->name, EXTERNAL_ACCT))
|
|
continue;
|
|
|
|
/* Add it to the result data */
|
|
json_object_start(res, NULL);
|
|
|
|
json_add_string(res, "account", accts[i]->name);
|
|
if (accts[i]->peer_id) {
|
|
json_add_node_id(res, "peer_id", accts[i]->peer_id);
|
|
json_add_bool(res, "we_opened", accts[i]->we_opened);
|
|
json_add_bool(res, "account_closed",
|
|
!(!accts[i]->closed_event_db_id));
|
|
json_add_bool(res, "account_resolved",
|
|
accts[i]->onchain_resolved_block > 0);
|
|
if (accts[i]->onchain_resolved_block > 0)
|
|
json_add_u32(res, "resolved_at_block",
|
|
accts[i]->onchain_resolved_block);
|
|
}
|
|
|
|
json_array_start(res, "balances");
|
|
for (size_t j = 0; j < tal_count(balances); j++) {
|
|
json_object_start(res, NULL);
|
|
json_add_amount_msat_only(res, "balance_msat",
|
|
balances[j]->balance);
|
|
json_add_string(res, "coin_type",
|
|
balances[j]->currency);
|
|
json_object_end(res);
|
|
}
|
|
json_array_end(res);
|
|
|
|
json_object_end(res);
|
|
}
|
|
json_array_end(res);
|
|
db_commit_transaction(db);
|
|
|
|
return command_finished(cmd, res);
|
|
}
|
|
|
|
struct new_account_info {
|
|
struct account *acct;
|
|
struct amount_msat curr_bal;
|
|
u32 timestamp;
|
|
char *currency;
|
|
};
|
|
|
|
static void try_update_open_fees(struct command *cmd,
|
|
struct account *acct)
|
|
{
|
|
struct chain_event *ev;
|
|
char *err;
|
|
|
|
assert(acct->closed_event_db_id);
|
|
ev = find_chain_event_by_id(cmd, db, *acct->closed_event_db_id);
|
|
assert(ev);
|
|
|
|
err = maybe_update_onchain_fees(cmd, db, ev->spending_txid);
|
|
if (err)
|
|
plugin_err(cmd->plugin,
|
|
"failure updating chain fees:"
|
|
" %s", err);
|
|
|
|
}
|
|
|
|
static void find_push_amts(const char *buf,
|
|
const jsmntok_t *curr_chan,
|
|
bool is_opener,
|
|
struct amount_msat *push_credit,
|
|
struct amount_msat *push_debit,
|
|
bool *is_leased)
|
|
{
|
|
const char *err;
|
|
struct amount_msat push_amt;
|
|
|
|
/* Try to pull out fee_rcvd_msat */
|
|
err = json_scan(tmpctx, buf, curr_chan,
|
|
"{funding:{fee_rcvd_msat:%}}",
|
|
JSON_SCAN(json_to_msat,
|
|
push_credit));
|
|
|
|
if (!err) {
|
|
*is_leased = true;
|
|
*push_debit = AMOUNT_MSAT(0);
|
|
return;
|
|
}
|
|
|
|
/* Try to pull out fee_paid_msat */
|
|
err = json_scan(tmpctx, buf, curr_chan,
|
|
"{funding:{fee_paid_msat:%}}",
|
|
JSON_SCAN(json_to_msat,
|
|
push_debit));
|
|
if (!err) {
|
|
*is_leased = true;
|
|
*push_credit = AMOUNT_MSAT(0);
|
|
return;
|
|
}
|
|
|
|
/* Try to pull out pushed amt? */
|
|
err = json_scan(tmpctx, buf, curr_chan,
|
|
"{funding:{pushed_msat:%}}",
|
|
JSON_SCAN(json_to_msat, &push_amt));
|
|
|
|
if (!err) {
|
|
*is_leased = false;
|
|
if (is_opener) {
|
|
*push_credit = AMOUNT_MSAT(0);
|
|
*push_debit = push_amt;
|
|
} else {
|
|
*push_credit = push_amt;
|
|
*push_debit = AMOUNT_MSAT(0);
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Nothing pushed nor fees paid */
|
|
*is_leased = false;
|
|
*push_credit = AMOUNT_MSAT(0);
|
|
*push_debit = AMOUNT_MSAT(0);
|
|
}
|
|
|
|
static bool new_missed_channel_account(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *result,
|
|
struct account *acct,
|
|
const char *currency,
|
|
u64 timestamp)
|
|
{
|
|
struct chain_event *chain_ev;
|
|
size_t i, j;
|
|
const jsmntok_t *curr_peer, *curr_chan,
|
|
*peer_arr_tok, *chan_arr_tok;
|
|
|
|
peer_arr_tok = json_get_member(buf, result, "peers");
|
|
assert(peer_arr_tok->type == JSMN_ARRAY);
|
|
/* There should only be one peer */
|
|
json_for_each_arr(i, curr_peer, peer_arr_tok) {
|
|
const char *err;
|
|
struct node_id peer_id;
|
|
|
|
err = json_scan(cmd, buf, curr_peer, "{id:%}",
|
|
JSON_SCAN(json_to_node_id, &peer_id));
|
|
|
|
if (err)
|
|
plugin_err(cmd->plugin,
|
|
"failure scanning listpeer"
|
|
" result: %s", err);
|
|
|
|
json_get_member(buf, curr_peer, "id");
|
|
chan_arr_tok = json_get_member(buf, curr_peer,
|
|
"channels");
|
|
assert(chan_arr_tok->type == JSMN_ARRAY);
|
|
json_for_each_arr(j, curr_chan, chan_arr_tok) {
|
|
struct bitcoin_outpoint opt;
|
|
struct amount_msat amt, remote_amt,
|
|
push_credit, push_debit;
|
|
char *opener, *chan_id;
|
|
enum mvt_tag *tags;
|
|
bool ok, is_opener, is_leased;
|
|
|
|
err = json_scan(tmpctx, buf, curr_chan,
|
|
"{channel_id:%,"
|
|
"funding_txid:%,"
|
|
"funding_outnum:%,"
|
|
"funding:{local_funds_msat:%,"
|
|
"remote_funds_msat:%},"
|
|
"opener:%}",
|
|
JSON_SCAN_TAL(tmpctx, json_strdup, &chan_id),
|
|
JSON_SCAN(json_to_txid, &opt.txid),
|
|
JSON_SCAN(json_to_number, &opt.n),
|
|
JSON_SCAN(json_to_msat, &amt),
|
|
JSON_SCAN(json_to_msat, &remote_amt),
|
|
JSON_SCAN_TAL(tmpctx, json_strdup, &opener));
|
|
if (err)
|
|
plugin_err(cmd->plugin,
|
|
"failure scanning listpeer"
|
|
" result: %s", err);
|
|
|
|
if (!streq(chan_id, acct->name))
|
|
continue;
|
|
|
|
plugin_log(cmd->plugin, LOG_DBG,
|
|
"Logging channel account from list %s",
|
|
acct->name);
|
|
|
|
chain_ev = tal(cmd, struct chain_event);
|
|
chain_ev->tag = mvt_tag_str(CHANNEL_OPEN);
|
|
chain_ev->debit = AMOUNT_MSAT(0);
|
|
ok = amount_msat_add(&chain_ev->output_value,
|
|
amt, remote_amt);
|
|
assert(ok);
|
|
chain_ev->currency = tal_strdup(chain_ev, currency);
|
|
chain_ev->origin_acct = NULL;
|
|
/* 2s before the channel opened, minimum */
|
|
chain_ev->timestamp = timestamp - 2;
|
|
chain_ev->blockheight = 0;
|
|
chain_ev->outpoint = opt;
|
|
chain_ev->spending_txid = NULL;
|
|
chain_ev->payment_id = NULL;
|
|
chain_ev->ignored = false;
|
|
chain_ev->stealable = false;
|
|
chain_ev->desc = NULL;
|
|
|
|
/* Update the account info too */
|
|
tags = tal_arr(chain_ev, enum mvt_tag, 1);
|
|
tags[0] = CHANNEL_OPEN;
|
|
|
|
is_opener = streq(opener, "local");
|
|
|
|
/* Leased/pushed channels have some extra work */
|
|
find_push_amts(buf, curr_chan, is_opener,
|
|
&push_credit, &push_debit,
|
|
&is_leased);
|
|
|
|
if (is_leased)
|
|
tal_arr_expand(&tags, LEASED);
|
|
if (is_opener)
|
|
tal_arr_expand(&tags, OPENER);
|
|
|
|
chain_ev->credit = amt;
|
|
db_begin_transaction(db);
|
|
if (!log_chain_event(db, acct, chain_ev))
|
|
goto done;
|
|
|
|
maybe_update_account(db, acct, chain_ev,
|
|
tags, 0, &peer_id);
|
|
maybe_update_onchain_fees(cmd, db, &opt.txid);
|
|
|
|
/* We won't count the close's fees if we're
|
|
* *not* the opener, which we didn't know
|
|
* until now, so now try to update the
|
|
* fees for the close tx's spending_txid..*/
|
|
if (acct->closed_event_db_id)
|
|
try_update_open_fees(cmd, acct);
|
|
|
|
/* We log a channel event for the push amt */
|
|
if (!amount_msat_zero(push_credit)
|
|
|| !amount_msat_zero(push_debit)) {
|
|
struct channel_event *chan_ev;
|
|
char *chan_tag;
|
|
|
|
chan_tag = tal_fmt(tmpctx, "%s",
|
|
mvt_tag_str(
|
|
is_leased ?
|
|
LEASE_FEE : PUSHED));
|
|
|
|
chan_ev = new_channel_event(tmpctx,
|
|
chan_tag,
|
|
push_credit,
|
|
push_debit,
|
|
AMOUNT_MSAT(0),
|
|
currency,
|
|
NULL, 0,
|
|
timestamp - 1);
|
|
log_channel_event(db, acct, chan_ev);
|
|
}
|
|
|
|
done:
|
|
db_commit_transaction(db);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Net out credit/debit --> basically find the diff */
|
|
static char *msat_net(const tal_t *ctx,
|
|
struct amount_msat credit,
|
|
struct amount_msat debit,
|
|
struct amount_msat *credit_net,
|
|
struct amount_msat *debit_net)
|
|
{
|
|
if (amount_msat_eq(credit, debit)) {
|
|
*credit_net = AMOUNT_MSAT(0);
|
|
*debit_net = AMOUNT_MSAT(0);
|
|
} else if (amount_msat_greater(credit, debit)) {
|
|
if (!amount_msat_sub(credit_net, credit, debit))
|
|
return tal_fmt(ctx, "unexpected fail, can't sub."
|
|
" %s - %s",
|
|
type_to_string(ctx, struct amount_msat,
|
|
&credit),
|
|
type_to_string(ctx, struct amount_msat,
|
|
&debit));
|
|
*debit_net = AMOUNT_MSAT(0);
|
|
} else {
|
|
if (!amount_msat_sub(debit_net, debit, credit)) {
|
|
return tal_fmt(ctx, "unexpected fail, can't sub."
|
|
" %s - %s",
|
|
type_to_string(ctx,
|
|
struct amount_msat,
|
|
&debit),
|
|
type_to_string(ctx,
|
|
struct amount_msat,
|
|
&credit));
|
|
}
|
|
*credit_net = AMOUNT_MSAT(0);
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static char *msat_find_diff(struct amount_msat balance,
|
|
struct amount_msat credits,
|
|
struct amount_msat debits,
|
|
struct amount_msat *credit_diff,
|
|
struct amount_msat *debit_diff)
|
|
{
|
|
struct amount_msat net_credit, net_debit;
|
|
char *err;
|
|
|
|
err = msat_net(tmpctx, credits, debits,
|
|
&net_credit, &net_debit);
|
|
if (err)
|
|
return err;
|
|
|
|
/* If we're not missing events, debits == 0 */
|
|
if (!amount_msat_zero(net_debit)) {
|
|
assert(amount_msat_zero(net_credit));
|
|
if (!amount_msat_add(credit_diff, net_debit, balance))
|
|
return "Overflow finding credit_diff";
|
|
*debit_diff = AMOUNT_MSAT(0);
|
|
} else {
|
|
assert(amount_msat_zero(net_debit));
|
|
if (amount_msat_greater(net_credit, balance)) {
|
|
if (!amount_msat_sub(debit_diff, net_credit,
|
|
balance))
|
|
return "Err net_credit - amt";
|
|
*credit_diff = AMOUNT_MSAT(0);
|
|
} else {
|
|
if (!amount_msat_sub(credit_diff, balance,
|
|
net_credit))
|
|
return "Err amt - net_credit";
|
|
|
|
*debit_diff = AMOUNT_MSAT(0);
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void log_journal_entry(struct account *acct,
|
|
const char *currency,
|
|
u64 timestamp,
|
|
struct amount_msat credit_diff,
|
|
struct amount_msat debit_diff)
|
|
{
|
|
struct channel_event *chan_ev;
|
|
|
|
/* No diffs to register, no journal needed */
|
|
if (amount_msat_zero(credit_diff)
|
|
&& amount_msat_zero(debit_diff))
|
|
return;
|
|
|
|
chan_ev = new_channel_event(tmpctx,
|
|
tal_fmt(tmpctx, "%s",
|
|
account_entry_tag_str(JOURNAL_ENTRY)),
|
|
credit_diff,
|
|
debit_diff,
|
|
AMOUNT_MSAT(0),
|
|
currency,
|
|
NULL, 0,
|
|
timestamp);
|
|
db_begin_transaction(db);
|
|
log_channel_event(db, acct, chan_ev);
|
|
db_commit_transaction(db);
|
|
}
|
|
|
|
static struct command_result *log_error(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *error,
|
|
void *arg UNNEEDED)
|
|
{
|
|
plugin_log(cmd->plugin, LOG_BROKEN,
|
|
"error calling rpc: %.*s",
|
|
json_tok_full_len(error),
|
|
json_tok_full(buf, error));
|
|
|
|
return notification_handled(cmd);
|
|
}
|
|
|
|
static struct command_result *
|
|
listpeers_multi_done(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *result,
|
|
struct new_account_info **new_accts)
|
|
{
|
|
/* Let's register all these accounts! */
|
|
for (size_t i = 0; i < tal_count(new_accts); i++) {
|
|
struct new_account_info *info = new_accts[i];
|
|
struct acct_balance **balances, *bal;
|
|
struct amount_msat credit_diff, debit_diff;
|
|
char *err;
|
|
|
|
if (!new_missed_channel_account(cmd, buf, result,
|
|
info->acct,
|
|
info->currency,
|
|
info->timestamp)) {
|
|
plugin_log(cmd->plugin, LOG_BROKEN,
|
|
"Unable to find account %s in listpeers",
|
|
info->acct->name);
|
|
continue;
|
|
}
|
|
|
|
db_begin_transaction(db);
|
|
err = account_get_balance(tmpctx, db, info->acct->name,
|
|
false, false, &balances, NULL);
|
|
db_commit_transaction(db);
|
|
|
|
if (err)
|
|
plugin_err(cmd->plugin, err);
|
|
|
|
/* FIXME: multiple currencies */
|
|
if (tal_count(balances) > 0)
|
|
bal = balances[0];
|
|
else {
|
|
bal = tal(tmpctx, struct acct_balance);
|
|
bal->credit = AMOUNT_MSAT(0);
|
|
bal->debit= AMOUNT_MSAT(0);
|
|
}
|
|
|
|
err = msat_find_diff(info->curr_bal,
|
|
bal->credit,
|
|
bal->debit,
|
|
&credit_diff, &debit_diff);
|
|
if (err)
|
|
plugin_err(cmd->plugin, err);
|
|
|
|
log_journal_entry(info->acct,
|
|
info->currency,
|
|
info->timestamp - 1,
|
|
credit_diff, debit_diff);
|
|
}
|
|
|
|
plugin_log(cmd->plugin, LOG_DBG, "Snapshot balances updated");
|
|
return notification_handled(cmd);
|
|
}
|
|
|
|
static char *do_account_close_checks(const tal_t *ctx,
|
|
struct chain_event *e,
|
|
struct account *acct)
|
|
{
|
|
struct account *closed_acct;
|
|
|
|
db_begin_transaction(db);
|
|
|
|
/* If is an external acct event, might be close channel related */
|
|
if (!is_channel_account(acct) && e->origin_acct) {
|
|
closed_acct = find_account(ctx, db, e->origin_acct);
|
|
} else if (!is_channel_account(acct) && !e->spending_txid)
|
|
closed_acct = find_close_account(ctx, db, &e->outpoint.txid);
|
|
else
|
|
/* Get most up to date account entry */
|
|
closed_acct = find_account(ctx, db, acct->name);
|
|
|
|
|
|
if (closed_acct && closed_acct->closed_event_db_id) {
|
|
maybe_mark_account_onchain(db, closed_acct);
|
|
if (closed_acct->onchain_resolved_block > 0) {
|
|
char *err;
|
|
err = update_channel_onchain_fees(ctx, db, closed_acct);
|
|
if (err) {
|
|
db_commit_transaction(db);
|
|
return err;
|
|
}
|
|
}
|
|
}
|
|
|
|
db_commit_transaction(db);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct command_result *json_balance_snapshot(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params)
|
|
{
|
|
const char *err;
|
|
size_t i;
|
|
u32 blockheight;
|
|
u64 timestamp;
|
|
struct new_account_info **new_accts;
|
|
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,
|
|
"{blockheight:%"
|
|
",timestamp:%}",
|
|
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));
|
|
|
|
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));
|
|
|
|
new_accts = tal_arr(cmd, struct new_account_info *, 0);
|
|
|
|
db_begin_transaction(db);
|
|
json_for_each_arr(i, acct_tok, accounts_tok) {
|
|
struct acct_balance **balances, *bal;
|
|
struct amount_msat snap_balance, credit_diff, debit_diff;
|
|
char *acct_name, *currency;
|
|
bool exists;
|
|
|
|
err = json_scan(cmd, buf, acct_tok,
|
|
"{account_id:%"
|
|
",balance_msat:%"
|
|
",coin_type:%}",
|
|
JSON_SCAN_TAL(tmpctx, json_strdup, &acct_name),
|
|
JSON_SCAN(json_to_msat, &snap_balance),
|
|
JSON_SCAN_TAL(tmpctx, json_strdup,
|
|
¤cy));
|
|
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",
|
|
acct_name,
|
|
type_to_string(tmpctx, struct amount_msat,
|
|
&snap_balance));
|
|
|
|
/* Find the account balances */
|
|
err = account_get_balance(cmd, db, acct_name,
|
|
/* Don't error if negative */
|
|
false,
|
|
/* Ignore non-clightning
|
|
* balances items */
|
|
true,
|
|
&balances,
|
|
&exists);
|
|
|
|
if (err)
|
|
plugin_err(cmd->plugin,
|
|
"Get account balance returned err"
|
|
" for account %s: %s",
|
|
acct_name, err);
|
|
|
|
/* FIXME: multiple currency balances */
|
|
if (tal_count(balances) > 0)
|
|
bal = balances[0];
|
|
else {
|
|
bal = tal(balances, struct acct_balance);
|
|
bal->credit = AMOUNT_MSAT(0);
|
|
bal->debit = AMOUNT_MSAT(0);
|
|
}
|
|
|
|
/* Figure out what the net diff is btw reported & actual */
|
|
err = msat_find_diff(snap_balance,
|
|
bal->credit,
|
|
bal->debit,
|
|
&credit_diff, &debit_diff);
|
|
if (err)
|
|
plugin_err(cmd->plugin,
|
|
"Unable to find_diff for amounts: %s",
|
|
err);
|
|
|
|
if (!exists
|
|
|| !amount_msat_zero(credit_diff)
|
|
|| !amount_msat_zero(debit_diff)) {
|
|
struct account *acct;
|
|
struct channel_event *ev;
|
|
u64 timestamp_now;
|
|
|
|
/* This is *expected* on first run of bookkeeper! */
|
|
plugin_log(cmd->plugin,
|
|
tom_jones ? LOG_DBG : LOG_UNUSUAL,
|
|
"Snapshot balance does not equal ondisk"
|
|
" reported %s, off by (+%s/-%s) (account %s)"
|
|
" Logging journal entry.",
|
|
type_to_string(tmpctx, struct amount_msat,
|
|
&snap_balance),
|
|
type_to_string(tmpctx, struct amount_msat,
|
|
&debit_diff),
|
|
type_to_string(tmpctx, struct amount_msat,
|
|
&credit_diff),
|
|
acct_name);
|
|
|
|
timestamp_now = time_now().ts.tv_sec;
|
|
|
|
/* Log a channel "journal entry" to get
|
|
* the balances inline */
|
|
acct = find_account(cmd, db, acct_name);
|
|
if (!acct) {
|
|
struct new_account_info *info;
|
|
|
|
plugin_log(cmd->plugin, LOG_INFORM,
|
|
"account %s not found, adding"
|
|
" along with new balance",
|
|
acct_name);
|
|
|
|
/* FIXME: lookup peer id for channel? */
|
|
acct = new_account(cmd, acct_name, NULL);
|
|
account_add(db, acct);
|
|
|
|
/* If we're entering a channel account,
|
|
* from a balance entry, we need to
|
|
* go find the channel open info*/
|
|
if (is_channel_account(acct)) {
|
|
info = tal(new_accts, struct new_account_info);
|
|
info->acct = tal_steal(info, acct);
|
|
info->curr_bal = snap_balance;
|
|
info->timestamp = timestamp_now;
|
|
info->currency =
|
|
tal_strdup(info, currency);
|
|
|
|
tal_arr_expand(&new_accts, info);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
ev = new_channel_event(cmd,
|
|
tal_fmt(tmpctx, "%s",
|
|
account_entry_tag_str(JOURNAL_ENTRY)),
|
|
credit_diff,
|
|
debit_diff,
|
|
AMOUNT_MSAT(0),
|
|
currency,
|
|
NULL, 0,
|
|
timestamp);
|
|
|
|
log_channel_event(db, acct, ev);
|
|
}
|
|
}
|
|
db_commit_transaction(db);
|
|
|
|
if (tal_count(new_accts) > 0) {
|
|
struct out_req *req;
|
|
|
|
req = jsonrpc_request_start(cmd->plugin, cmd,
|
|
"listpeers",
|
|
listpeers_multi_done,
|
|
log_error,
|
|
new_accts);
|
|
return send_outreq(cmd->plugin, req);
|
|
}
|
|
|
|
plugin_log(cmd->plugin, LOG_DBG, "Snapshot balances updated");
|
|
return notification_handled(cmd);
|
|
}
|
|
|
|
/* Returns true if "fatal" error, otherwise just a normal error */
|
|
static char *fetch_out_desc_invstr(const tal_t *ctx, const char *buf,
|
|
const jsmntok_t *tok, char **err)
|
|
{
|
|
char *bolt, *desc, *fail;
|
|
|
|
/* It's a bolt11! Parse it out to a desc */
|
|
if (!json_scan(ctx, buf, tok, "{bolt11:%}",
|
|
JSON_SCAN_TAL(ctx, json_strdup, &bolt))) {
|
|
struct bolt11 *bolt11;
|
|
u5 *sigdata;
|
|
struct sha256 hash;
|
|
bool have_n;
|
|
|
|
bolt11 = bolt11_decode_nosig(ctx, bolt,
|
|
/* No desc/features/chain checks */
|
|
NULL, NULL, NULL,
|
|
&hash, &sigdata, &have_n,
|
|
&fail);
|
|
|
|
if (bolt11) {
|
|
if (bolt11->description)
|
|
desc = tal_strdup(ctx, bolt11->description);
|
|
else if (bolt11->description_hash)
|
|
desc = tal_fmt(ctx, "%s",
|
|
type_to_string(ctx,
|
|
struct sha256,
|
|
bolt11->description_hash));
|
|
else
|
|
desc = NULL;
|
|
} else {
|
|
*err = tal_fmt(ctx, "failed to parse bolt11 %s: %s",
|
|
bolt, fail);
|
|
return NULL;
|
|
}
|
|
} else if (!json_scan(ctx, buf, tok, "{bolt12:%}",
|
|
JSON_SCAN_TAL(ctx, json_strdup, &bolt))) {
|
|
struct tlv_invoice *bolt12;
|
|
|
|
bolt12 = invoice_decode_nosig(ctx, bolt, strlen(bolt),
|
|
/* No features/chain checks */
|
|
NULL, NULL,
|
|
&fail);
|
|
if (!bolt12) {
|
|
*err = tal_fmt(ctx, "failed to parse"
|
|
" bolt12 %s: %s",
|
|
bolt, fail);
|
|
return NULL;
|
|
}
|
|
|
|
if (bolt12->description)
|
|
desc = tal_strndup(ctx,
|
|
cast_signed(char *, bolt12->description),
|
|
tal_bytelen(bolt12->description));
|
|
else
|
|
desc = NULL;
|
|
} else
|
|
desc = NULL;
|
|
|
|
*err = NULL;
|
|
return desc;
|
|
}
|
|
|
|
static struct command_result *
|
|
listinvoice_done(struct command *cmd, const char *buf,
|
|
const jsmntok_t *result, struct sha256 *payment_hash)
|
|
{
|
|
size_t i;
|
|
const jsmntok_t *inv_arr_tok, *inv_tok;
|
|
const char *desc;
|
|
inv_arr_tok = json_get_member(buf, result, "invoices");
|
|
assert(inv_arr_tok->type == JSMN_ARRAY);
|
|
|
|
desc = NULL;
|
|
json_for_each_arr(i, inv_tok, inv_arr_tok) {
|
|
char *err;
|
|
|
|
/* Found desc in "description" */
|
|
if (!json_scan(cmd, buf, inv_tok, "{description:%}",
|
|
JSON_SCAN_TAL(cmd, json_strdup, &desc)))
|
|
break;
|
|
|
|
/* if 'description' doesn't exist, try bolt11/bolt12 */
|
|
desc = fetch_out_desc_invstr(cmd, buf, inv_tok, &err);
|
|
if (desc || err) {
|
|
if (err)
|
|
plugin_log(cmd->plugin,
|
|
LOG_BROKEN, "%s", err);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (desc) {
|
|
db_begin_transaction(db);
|
|
add_payment_hash_desc(db, payment_hash,
|
|
json_escape_unescape(cmd,
|
|
(struct json_escape *)desc));
|
|
db_commit_transaction(db);
|
|
} else
|
|
plugin_log(cmd->plugin, LOG_DBG,
|
|
"listinvoices:"
|
|
" description/bolt11/bolt12"
|
|
" not found (%.*s)",
|
|
result->end - result->start, buf);
|
|
|
|
return notification_handled(cmd);
|
|
}
|
|
|
|
static struct command_result *
|
|
listsendpays_done(struct command *cmd, const char *buf,
|
|
const jsmntok_t *result, struct sha256 *payment_hash)
|
|
{
|
|
size_t i;
|
|
const jsmntok_t *pays_arr_tok, *pays_tok;
|
|
const char *desc;
|
|
pays_arr_tok = json_get_member(buf, result, "payments");
|
|
assert(pays_arr_tok->type == JSMN_ARRAY);
|
|
|
|
/* Did we find a matching entry? */
|
|
desc = NULL;
|
|
json_for_each_arr(i, pays_tok, pays_arr_tok) {
|
|
char *err;
|
|
|
|
desc = fetch_out_desc_invstr(cmd, buf, pays_tok, &err);
|
|
if (desc || err) {
|
|
if (err)
|
|
plugin_log(cmd->plugin,
|
|
LOG_BROKEN, "%s", err);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (desc) {
|
|
db_begin_transaction(db);
|
|
add_payment_hash_desc(db, payment_hash, desc);
|
|
db_commit_transaction(db);
|
|
} else
|
|
plugin_log(cmd->plugin, LOG_DBG,
|
|
"listpays: bolt11/bolt12 not found:"
|
|
"(%.*s)",
|
|
result->end - result->start, buf);
|
|
|
|
return notification_handled(cmd);
|
|
}
|
|
|
|
static struct command_result *lookup_invoice_desc(struct command *cmd,
|
|
struct amount_msat credit,
|
|
struct sha256 *payment_hash STEALS)
|
|
{
|
|
struct out_req *req;
|
|
|
|
/* Otherwise will go away when event is cleaned up */
|
|
tal_steal(cmd, payment_hash);
|
|
if (!amount_msat_zero(credit))
|
|
req = jsonrpc_request_start(cmd->plugin, cmd,
|
|
"listinvoices",
|
|
listinvoice_done,
|
|
log_error,
|
|
payment_hash);
|
|
else
|
|
req = jsonrpc_request_start(cmd->plugin, cmd,
|
|
"listsendpays",
|
|
listsendpays_done,
|
|
log_error,
|
|
payment_hash);
|
|
|
|
json_add_sha256(req->js, "payment_hash", payment_hash);
|
|
return send_outreq(cmd->plugin, req);
|
|
}
|
|
|
|
struct event_info {
|
|
struct chain_event *ev;
|
|
struct account *acct;
|
|
};
|
|
|
|
static struct command_result *
|
|
listpeers_done(struct command *cmd, const char *buf,
|
|
const jsmntok_t *result, struct event_info *info)
|
|
{
|
|
struct acct_balance **balances, *bal;
|
|
struct amount_msat credit_diff, debit_diff;
|
|
const char *err;
|
|
|
|
if (new_missed_channel_account(cmd, buf, result,
|
|
info->acct,
|
|
info->ev->currency,
|
|
info->ev->timestamp)) {
|
|
db_begin_transaction(db);
|
|
err = account_get_balance(tmpctx, db, info->acct->name,
|
|
false, false, &balances, NULL);
|
|
db_commit_transaction(db);
|
|
|
|
if (err)
|
|
plugin_err(cmd->plugin, err);
|
|
|
|
/* FIXME: multiple currencies per account? */
|
|
if (tal_count(balances) > 0)
|
|
bal = balances[0];
|
|
else {
|
|
bal = tal(balances, struct acct_balance);
|
|
bal->credit = AMOUNT_MSAT(0);
|
|
bal->debit = AMOUNT_MSAT(0);
|
|
}
|
|
assert(tal_count(balances) == 1);
|
|
|
|
/* The expected current balance is zero, since
|
|
* we just got the channel close event */
|
|
err = msat_find_diff(AMOUNT_MSAT(0),
|
|
bal->credit,
|
|
bal->debit,
|
|
&credit_diff, &debit_diff);
|
|
if (err)
|
|
plugin_err(cmd->plugin, err);
|
|
|
|
log_journal_entry(info->acct,
|
|
info->ev->currency,
|
|
info->ev->timestamp - 1,
|
|
credit_diff, debit_diff);
|
|
} else
|
|
plugin_log(cmd->plugin, LOG_BROKEN,
|
|
"Unable to find account %s in listpeers",
|
|
info->acct->name);
|
|
|
|
/* Maybe mark acct as onchain resolved */
|
|
err = do_account_close_checks(cmd, info->ev, info->acct);
|
|
if (err)
|
|
plugin_err(cmd->plugin, err);
|
|
|
|
if (info->ev->payment_id &&
|
|
streq(info->ev->tag, mvt_tag_str(INVOICE))) {
|
|
return lookup_invoice_desc(cmd, info->ev->credit,
|
|
info->ev->payment_id);
|
|
}
|
|
|
|
return notification_handled(cmd);
|
|
}
|
|
static struct command_result *
|
|
parse_and_log_chain_move(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params,
|
|
const char *acct_name STEALS,
|
|
const struct amount_msat credit,
|
|
const struct amount_msat debit,
|
|
const char *coin_type STEALS,
|
|
const u64 timestamp,
|
|
const enum mvt_tag *tags,
|
|
const char *desc)
|
|
{
|
|
struct chain_event *e = tal(cmd, struct chain_event);
|
|
struct sha256 *payment_hash = tal(cmd, struct sha256);
|
|
struct bitcoin_txid *spending_txid = tal(cmd, struct bitcoin_txid);
|
|
struct node_id *peer_id;
|
|
struct account *acct, *orig_acct;
|
|
u32 closed_count;
|
|
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, &e->outpoint.txid),
|
|
JSON_SCAN(json_to_number, &e->outpoint.n),
|
|
JSON_SCAN(json_to_msat, &e->output_value),
|
|
JSON_SCAN(json_to_number, &e->blockheight));
|
|
|
|
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));
|
|
|
|
/* 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);
|
|
err = tal_free(err);
|
|
}
|
|
|
|
e->spending_txid = tal_steal(e, 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);
|
|
err = tal_free(err);
|
|
}
|
|
|
|
err = json_scan(tmpctx, buf, params,
|
|
"{coin_movement:"
|
|
"{originating_account:%}}",
|
|
JSON_SCAN_TAL(e, json_strdup, &e->origin_acct));
|
|
|
|
if (err) {
|
|
e->origin_acct = NULL;
|
|
err = tal_free(err);
|
|
}
|
|
|
|
peer_id = tal(cmd, struct node_id);
|
|
err = json_scan(tmpctx, buf, params,
|
|
"{coin_movement:"
|
|
"{peer_id:%}}",
|
|
JSON_SCAN(json_to_node_id, peer_id));
|
|
|
|
if (err) {
|
|
peer_id = tal_free(peer_id);
|
|
err = tal_free(err);
|
|
}
|
|
|
|
err = json_scan(tmpctx, buf, params,
|
|
"{coin_movement:"
|
|
"{output_count:%}}",
|
|
JSON_SCAN(json_to_number, &closed_count));
|
|
|
|
if (err) {
|
|
closed_count = 0;
|
|
err = tal_free(err);
|
|
}
|
|
|
|
e->payment_id = tal_steal(e, payment_hash);
|
|
|
|
e->credit = credit;
|
|
e->debit = debit;
|
|
e->currency = tal_steal(e, coin_type);
|
|
e->timestamp = timestamp;
|
|
e->tag = mvt_tag_str(tags[0]);
|
|
e->desc = tal_steal(e, desc);
|
|
|
|
e->ignored = false;
|
|
e->stealable = false;
|
|
for (size_t i = 0; i < tal_count(tags); i++) {
|
|
e->ignored |= tags[i] == IGNORED;
|
|
e->stealable |= tags[i] == STEALABLE;
|
|
}
|
|
|
|
db_begin_transaction(db);
|
|
acct = find_account(tmpctx, db, acct_name);
|
|
|
|
if (!acct) {
|
|
/* FIXME: lookup the peer id for this channel! */
|
|
acct = new_account(tmpctx, acct_name, NULL);
|
|
account_add(db, acct);
|
|
}
|
|
|
|
if (e->origin_acct) {
|
|
orig_acct = find_account(tmpctx, db, e->origin_acct);
|
|
/* Go fetch the originating account
|
|
* (we might not have it) */
|
|
if (!orig_acct) {
|
|
orig_acct = new_account(tmpctx, e->origin_acct, NULL);
|
|
account_add(db, orig_acct);
|
|
}
|
|
} else
|
|
orig_acct = NULL;
|
|
|
|
|
|
if (!log_chain_event(db, acct, e)) {
|
|
db_commit_transaction(db);
|
|
/* This is not a new event, do nothing */
|
|
return notification_handled(cmd);
|
|
}
|
|
|
|
/* This event *might* have implications for account;
|
|
* update as necessary */
|
|
maybe_update_account(db, acct, e, tags, closed_count,
|
|
peer_id);
|
|
|
|
/* Can we calculate any onchain fees now? */
|
|
err = maybe_update_onchain_fees(cmd, db,
|
|
e->spending_txid ?
|
|
e->spending_txid :
|
|
&e->outpoint.txid);
|
|
|
|
db_commit_transaction(db);
|
|
|
|
if (err)
|
|
plugin_err(cmd->plugin,
|
|
"Unable to update onchain fees %s",
|
|
err);
|
|
|
|
/* If this is a spend confirmation event, it's possible
|
|
* that it we've got an external deposit that's now
|
|
* confirmed */
|
|
if (e->spending_txid) {
|
|
db_begin_transaction(db);
|
|
/* Go see if there's any deposits to an external
|
|
* that are now confirmed */
|
|
/* FIXME: might need updating when we can splice? */
|
|
maybe_closeout_external_deposits(db, e);
|
|
db_commit_transaction(db);
|
|
}
|
|
|
|
/* If this is a channel account event, it's possible
|
|
* that we *never* got the open event. (This happens
|
|
* if you add the plugin *after* you've closed the channel) */
|
|
if ((!acct->open_event_db_id && is_channel_account(acct))
|
|
|| (orig_acct && is_channel_account(orig_acct)
|
|
&& !orig_acct->open_event_db_id)) {
|
|
/* Find the channel open info for this peer */
|
|
struct out_req *req;
|
|
struct event_info *info;
|
|
|
|
plugin_log(cmd->plugin, LOG_DBG,
|
|
"channel event received but no open for channel %s."
|
|
" Calling `listpeers` to fetch missing info",
|
|
acct->name);
|
|
|
|
info = tal(cmd, struct event_info);
|
|
info->ev = tal_steal(info, e);
|
|
info->acct = tal_steal(info,
|
|
is_channel_account(acct) ?
|
|
acct : orig_acct);
|
|
|
|
req = jsonrpc_request_start(cmd->plugin, cmd,
|
|
"listpeers",
|
|
listpeers_done,
|
|
log_error,
|
|
info);
|
|
/* FIXME: use the peer_id to reduce work here */
|
|
return send_outreq(cmd->plugin, req);
|
|
}
|
|
|
|
/* Maybe mark acct as onchain resolved */
|
|
err = do_account_close_checks(cmd, e, acct);
|
|
if (err)
|
|
plugin_err(cmd->plugin, err);
|
|
|
|
/* Check for invoice desc data, necessary */
|
|
if (e->payment_id) {
|
|
for (size_t i = 0; i < tal_count(tags); i++) {
|
|
if (tags[i] != INVOICE)
|
|
continue;
|
|
|
|
return lookup_invoice_desc(cmd, e->credit,
|
|
e->payment_id);
|
|
}
|
|
}
|
|
|
|
return notification_handled(cmd);;
|
|
}
|
|
|
|
static struct command_result *
|
|
parse_and_log_channel_move(struct command *cmd,
|
|
const char *buf,
|
|
const jsmntok_t *params,
|
|
const char *acct_name STEALS,
|
|
const struct amount_msat credit,
|
|
const struct amount_msat debit,
|
|
const char *coin_type STEALS,
|
|
const u64 timestamp,
|
|
const enum mvt_tag *tags,
|
|
const char *desc)
|
|
{
|
|
struct channel_event *e = tal(cmd, struct channel_event);
|
|
struct account *acct;
|
|
const char *err;
|
|
|
|
e->payment_id = tal(e, struct sha256);
|
|
err = json_scan(tmpctx, buf, params,
|
|
"{coin_movement:{payment_hash:%}}",
|
|
JSON_SCAN(json_to_sha256, e->payment_id));
|
|
if (err) {
|
|
e->payment_id = tal_free(e->payment_id);
|
|
err = tal_free(err);
|
|
}
|
|
|
|
err = json_scan(tmpctx, buf, params,
|
|
"{coin_movement:{part_id:%}}",
|
|
JSON_SCAN(json_to_number, &e->part_id));
|
|
if (err) {
|
|
e->part_id = 0;
|
|
err = tal_free(err);
|
|
}
|
|
|
|
err = json_scan(tmpctx, buf, params,
|
|
"{coin_movement:{fees_msat:%}}",
|
|
JSON_SCAN(json_to_msat, &e->fees));
|
|
if (err) {
|
|
e->fees = AMOUNT_MSAT(0);
|
|
err = tal_free(err);
|
|
}
|
|
|
|
e->credit = credit;
|
|
e->debit = debit;
|
|
e->currency = tal_steal(e, coin_type);
|
|
e->timestamp = timestamp;
|
|
e->tag = mvt_tag_str(tags[0]);
|
|
e->desc = tal_steal(e, desc);
|
|
e->rebalance_id = NULL;
|
|
|
|
/* Go find the account for this event */
|
|
db_begin_transaction(db);
|
|
acct = find_account(tmpctx, db, acct_name);
|
|
if (!acct)
|
|
plugin_err(cmd->plugin,
|
|
"Received channel event,"
|
|
" but no account exists %s",
|
|
acct_name);
|
|
|
|
log_channel_event(db, acct, e);
|
|
|
|
/* Check for invoice desc data, necessary */
|
|
if (e->payment_id) {
|
|
for (size_t i = 0; i < tal_count(tags); i++) {
|
|
if (tags[i] != INVOICE)
|
|
continue;
|
|
|
|
/* We only do rebalance checks for debits,
|
|
* the credit event always arrives first */
|
|
if (!amount_msat_zero(e->debit))
|
|
maybe_record_rebalance(db, e);
|
|
|
|
db_commit_transaction(db);
|
|
return lookup_invoice_desc(cmd, e->credit,
|
|
e->payment_id);
|
|
}
|
|
}
|
|
|
|
db_commit_transaction(db);
|
|
return notification_handled(cmd);
|
|
}
|
|
|
|
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;
|
|
u32 version;
|
|
u64 timestamp;
|
|
struct amount_msat credit, debit;
|
|
enum mvt_tag *tags;
|
|
|
|
err = json_scan(tmpctx, buf, params,
|
|
"{coin_movement:"
|
|
"{version:%"
|
|
",type:%"
|
|
",account_id:%"
|
|
",credit_msat:%"
|
|
",debit_msat:%"
|
|
",coin_type:%"
|
|
",timestamp:%"
|
|
"}}",
|
|
JSON_SCAN(json_to_number, &version),
|
|
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(tmpctx, 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 %s %"PRIu64,
|
|
version,
|
|
mvt_tag_str(tags[0]),
|
|
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))
|
|
return parse_and_log_chain_move(cmd, buf, params,
|
|
acct_name, credit, debit,
|
|
coin_type, timestamp, tags,
|
|
NULL);
|
|
|
|
|
|
assert(streq(mvt_type, CHANNEL_MOVE));
|
|
return parse_and_log_channel_move(cmd, buf, params,
|
|
acct_name, credit, debit,
|
|
coin_type, timestamp, tags,
|
|
NULL);
|
|
}
|
|
|
|
const struct plugin_notification notifs[] = {
|
|
{
|
|
"coin_movement",
|
|
json_coin_moved,
|
|
},
|
|
{
|
|
"balance_snapshot",
|
|
json_balance_snapshot,
|
|
}
|
|
};
|
|
|
|
static const struct plugin_command commands[] = {
|
|
{
|
|
"bkpr-listbalances",
|
|
"bookkeeping",
|
|
"List current account balances",
|
|
"List of current accounts and their balances",
|
|
json_list_balances
|
|
},
|
|
{
|
|
"bkpr-listaccountevents",
|
|
"bookkeeping",
|
|
"List all events for an {account}",
|
|
"List all events for an {account} (or all accounts, if"
|
|
" no account specified) in {format}. Sorted by timestamp",
|
|
json_list_account_events
|
|
},
|
|
{
|
|
"bkpr-inspect",
|
|
"utilities",
|
|
"See the current on-chain graph of an {account}",
|
|
"Prints out the on-chain footprint of a given {account}.",
|
|
json_inspect
|
|
},
|
|
{
|
|
"bkpr-listincome",
|
|
"bookkeeping",
|
|
"List all income impacting events",
|
|
"List all events for this node that impacted income",
|
|
json_list_income
|
|
},
|
|
{
|
|
"bkpr-dumpincomecsv",
|
|
"bookkeeping",
|
|
"Print out all the income events to a csv file in "
|
|
" {csv_format",
|
|
"Dump income statment data to {csv_file} in {csv_format}."
|
|
" Optionally, {consolidate_fee}s into single entries"
|
|
" (default: true)",
|
|
json_dump_income
|
|
},
|
|
{
|
|
"bkpr-channelsapy",
|
|
"bookkeeping",
|
|
"Stats on channel fund usage",
|
|
"Print out stats on chanenl fund usage",
|
|
json_channel_apy
|
|
},
|
|
};
|
|
|
|
static const char *init(struct plugin *p, const char *b, const jsmntok_t *t)
|
|
{
|
|
/* Switch to bookkeeper-dir, if specified */
|
|
if (datadir && chdir(datadir) != 0) {
|
|
if (mkdir(datadir, 0700) != 0 && errno != EEXIST)
|
|
plugin_err(p,
|
|
"Unable to create 'bookkeeper-dir'=%s",
|
|
datadir);
|
|
if (chdir(datadir) != 0)
|
|
plugin_err(p,
|
|
"Unable to switch to 'bookkeeper-dir'=%s",
|
|
datadir);
|
|
}
|
|
|
|
/* No user suppled db_dsn, set one up here */
|
|
if (!db_dsn)
|
|
db_dsn = tal_fmt(NULL, "sqlite3://accounts.sqlite3");
|
|
|
|
plugin_log(p, LOG_DBG, "Setting up database at %s", db_dsn);
|
|
/* Final flag tells us What's New, Pussycat. */
|
|
db = notleak(db_setup(p, p, db_dsn, &tom_jones));
|
|
db_dsn = tal_free(db_dsn);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
setup_locale();
|
|
|
|
/* No datadir is default */
|
|
datadir = NULL;
|
|
db_dsn = NULL;
|
|
|
|
plugin_main(argv, init, PLUGIN_STATIC, true, NULL,
|
|
commands, ARRAY_SIZE(commands),
|
|
notifs, ARRAY_SIZE(notifs),
|
|
NULL, 0,
|
|
NULL, 0,
|
|
plugin_option("bookkeeper-dir",
|
|
"string",
|
|
"Location for bookkeeper records.",
|
|
charp_option, &datadir),
|
|
plugin_option("bookkeeper-db",
|
|
"string",
|
|
"Location of the bookkeeper database",
|
|
charp_option, &db_dsn),
|
|
NULL);
|
|
|
|
return 0;
|
|
}
|