#include "config.h"
#include <bitcoin/chainparams.h>
#include <ccan/array_size/array_size.h>
#include <ccan/cast/cast.h>
#include <ccan/json_escape/json_escape.h>
#include <ccan/json_out/json_out.h>
#include <ccan/str/hex/hex.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/clock_time.h>
#include <common/coin_mvt.h>
#include <common/iso4217.h>
#include <common/json_param.h>
#include <common/json_stream.h>
#include <common/memleak.h>
#include <common/mkdatastorekey.h>
#include <common/node_id.h>
#include <db/exec.h>
#include <errno.h>
#include <inttypes.h>
#include <plugins/bkpr/account.h>
#include <plugins/bkpr/account_entry.h>
#include <plugins/bkpr/blockheights.h>
#include <plugins/bkpr/bookkeeper.h>
#include <plugins/bkpr/chain_event.h>
#include <plugins/bkpr/channel_event.h>
#include <plugins/bkpr/channelsapy.h>
#include <plugins/bkpr/descriptions.h>
#include <plugins/bkpr/incomestmt.h>
#include <plugins/bkpr/onchain_fee.h>
#include <plugins/bkpr/rebalances.h>
#include <plugins/bkpr/recorder.h>
#include <plugins/bkpr/report.h>
#include <plugins/libplugin.h>
#include <sys/stat.h>
#include <unistd.h>
#define CHAIN_MOVE "chain_mvt"
#define CHANNEL_MOVE "channel_mvt"
#define CURRENCYRATE_TOLERANCE_SECONDS 60
struct bkpr *bkpr_of(struct plugin *plugin)
{
return plugin_get_data(plugin, struct bkpr);
}
const struct currencyrate *covering_currencyrate(const struct bkpr *bkpr,
u64 timestamp)
{
u64 ts = timestamp + 1;
const struct currencyrate *crate = uintmap_before(bkpr->currency_rates, &ts);
if (crate && ts + crate->duration > timestamp)
return crate;
return NULL;
}
static u64 ratefactor(const struct iso4217_name_and_divisor *currency)
{
u64 mul = 1;
for (u32 i = 0; i < currency->minor_unit; i++)
mul *= 10;
return mul;
}
const char *currencyrate_str(const tal_t *ctx,
const struct bkpr *bkpr,
u64 timestamp,
const struct amount_msat *msat)
{
const struct currencyrate *crate;
u64 mul, intpart, fracpart, raw_rate;
crate = covering_currencyrate(bkpr, timestamp);
if (!crate)
return NULL;
mul = ratefactor(bkpr->currency);
if (msat) {
unsigned __int128 v;
v = (unsigned __int128)msat->millisatoshis * crate->raw_rate ;
raw_rate = v / MSAT_PER_BTC;
} else {
raw_rate = crate->raw_rate;
}
intpart = raw_rate / mul;
fracpart = raw_rate % mul;
if (bkpr->currency->minor_unit == 0)
return tal_fmt(ctx, "%"PRIu64, intpart);
return tal_fmt(ctx, "%"PRIu64".%0*"PRIu64,
intpart,
(int)bkpr->currency->minor_unit,
fracpart);
}
void json_add_currencyrate(struct json_stream *result,
const char *fieldname,
const struct bkpr *bkpr,
u64 timestamp)
{
const char *str = currencyrate_str(NULL, bkpr, timestamp, NULL);
if (str)
json_add_primitive(result, fieldname, take(str));
}
struct refresh_cb {
struct command *cmd;
struct command_result *(*cb)(struct command *, void *);
void *arg;
};
struct refresh_info {
size_t calls_remaining;
struct refresh_cb *callbacks;
};
static struct refresh_info *use_rinfo(struct refresh_info *rinfo)
{
rinfo->calls_remaining++;
return rinfo;
}
static struct command_result *limited_listchannelmoves(struct command *cmd,
struct refresh_info *rinfo);
static struct command_result *rinfo_one_done(struct command *cmd,
struct refresh_info *rinfo)
{
assert(rinfo->calls_remaining > 0);
if (--rinfo->calls_remaining == 0) {
struct command_result *first_ret = NULL;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
assert(rinfo == bkpr->rinfo);
bkpr->rinfo = NULL;
for (size_t i = 0; i < tal_count(rinfo->callbacks); i++) {
struct command_result *ret;
const struct refresh_cb *cb = &rinfo->callbacks[i];
ret = cb->cb(cb->cmd, cb->arg);
if (i == 0)
first_ret = ret;
}
tal_free(rinfo);
return first_ret;
}
return command_still_pending(cmd);
}
struct command_result *ignore_datastore_reply(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
void *arg)
{
return command_still_pending(cmd);
}
static void
parse_and_log_chain_move(struct command *cmd,
const char *buf,
const jsmntok_t *chainmove,
struct refresh_info *rinfo);
static void
parse_and_log_channel_move(struct command *cmd,
const char *buf,
const jsmntok_t *channelmove,
struct refresh_info *rinfo);
static struct command_result *datastore_done(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
struct refresh_info *rinfo)
{
return rinfo_one_done(cmd, rinfo);
}
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;
}
#define LISTCHANNELMOVES_LIMIT 10000
static struct command_result *listchannelmoves_done(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
struct refresh_info *rinfo)
{
const jsmntok_t *moves, *t;
size_t i;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
be64 be_index;
moves = json_get_member(buf, result, "channelmoves");
json_for_each_arr(i, t, moves)
parse_and_log_channel_move(cmd, buf, t, rinfo);
be_index = cpu_to_be64(bkpr->channelmoves_index);
jsonrpc_set_datastore_binary(cmd,
mkdatastorekey(tmpctx, "bookkeeper", "channelmoves_index"),
&be_index, sizeof(be_index),
"create-or-replace",
datastore_done, NULL, use_rinfo(rinfo));
if (moves->size == LISTCHANNELMOVES_LIMIT)
limited_listchannelmoves(cmd, rinfo);
return rinfo_one_done(cmd, rinfo);
}
static struct command_result *limited_listchannelmoves(struct command *cmd,
struct refresh_info *rinfo)
{
struct bkpr *bkpr = bkpr_of(cmd->plugin);
struct out_req *req;
req = jsonrpc_request_start(cmd, "listchannelmoves",
listchannelmoves_done,
plugin_broken_cb,
use_rinfo(rinfo));
json_add_string(req->js, "index", "created");
json_add_u64(req->js, "start", bkpr->channelmoves_index + 1);
json_add_u64(req->js, "limit", LISTCHANNELMOVES_LIMIT);
return send_outreq(req);
}
static struct command_result *listchainmoves_done(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
struct refresh_info *rinfo)
{
const jsmntok_t *moves, *t;
size_t i;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
be64 be_index;
moves = json_get_member(buf, result, "chainmoves");
json_for_each_arr(i, t, moves)
parse_and_log_chain_move(cmd, buf, t, rinfo);
be_index = cpu_to_be64(bkpr->chainmoves_index);
jsonrpc_set_datastore_binary(cmd,
mkdatastorekey(tmpctx, "bookkeeper", "chainmoves_index"),
&be_index, sizeof(be_index),
"create-or-replace",
datastore_done, NULL, use_rinfo(rinfo));
limited_listchannelmoves(cmd, rinfo);
return rinfo_one_done(cmd, rinfo);
}
static struct command_result *refresh_moves_(struct command *cmd,
struct command_result *(*cb)(
struct command *,
void *),
void *arg)
{
struct out_req *req;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
struct refresh_cb refresh_cb;
refresh_cb.cmd = cmd;
refresh_cb.cb = cb;
refresh_cb.arg = arg;
if (bkpr->rinfo) {
tal_arr_expand(&bkpr->rinfo->callbacks, refresh_cb);
return command_still_pending(cmd);
}
bkpr->rinfo = tal(bkpr, struct refresh_info);
bkpr->rinfo->calls_remaining = 0;
bkpr->rinfo->callbacks = tal_dup_arr(bkpr->rinfo, struct refresh_cb, &refresh_cb, 1, 0);
req = jsonrpc_request_start(cmd, "listchainmoves",
listchainmoves_done,
plugin_broken_cb,
use_rinfo(bkpr->rinfo));
json_add_string(req->js, "index", "created");
json_add_u64(req->js, "start", bkpr->chainmoves_index + 1);
return send_outreq(req);
}
#define refresh_moves(cmd, cb, arg) \
refresh_moves_((cmd), \
typesafe_cb_preargs(struct command_result *, void *, \
(cb), (arg), \
struct command *), \
arg)
struct apy_req {
u64 *start_time;
u64 *end_time;
};
static struct command_result *
getblockheight_done(struct command *cmd,
const char *method,
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;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
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 + result->start);
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 + result->start);
apys = compute_channel_apys(cmd, bkpr, cmd,
*req->start_time,
*req->end_time,
blockheight);
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]);
if (!channel_apy_sum(net_apys, apys[i]))
return command_fail(cmd, PLUGIN_ERROR,
"Overflow adding APYs net");
}
if (!amount_msat_is_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 *
do_channel_apy(struct command *cmd, struct apy_req *apyreq)
{
struct out_req *req;
req = jsonrpc_request_start(cmd, "getinfo",
&getblockheight_done,
forward_error,
apyreq);
return send_outreq(req);
}
static struct command_result *json_channel_apy(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
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();
return refresh_moves(cmd, do_channel_apy, apyreq);
}
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)));
}
struct dump_income_info {
struct csv_fmt *csv_fmt;
const char *filename;
bool *consolidate_fees;
u64 *start_time, *end_time;
};
static struct command_result *do_dump_income(struct command *cmd,
struct dump_income_info *info)
{
struct bkpr *bkpr = bkpr_of(cmd->plugin);
struct json_stream *res;
struct income_event **evs;
char *err;
evs = list_income_events(cmd, bkpr, cmd, *info->start_time, *info->end_time,
*info->consolidate_fees);
if (!info->filename)
info->filename = csv_filename(info, info->csv_fmt);
err = csv_print_income_events(cmd, info->csv_fmt, info->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", info->filename);
json_add_string(res, "csv_format", info->csv_fmt->fmt_name);
return command_finished(cmd, res);
}
static struct command_result *json_dump_income(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
struct dump_income_info *info = tal(cmd, struct dump_income_info);
if (!param(cmd, buf, params,
p_req("csv_format", param_csv_format, &info->csv_fmt),
p_opt("csv_file", param_string, &info->filename),
p_opt_def("consolidate_fees", param_bool,
&info->consolidate_fees, true),
p_opt_def("start_time", param_u64, &info->start_time, 0),
p_opt_def("end_time", param_u64, &info->end_time, SQLITE_MAX_UINT),
NULL))
return command_param_failed();
return refresh_moves(cmd, do_dump_income, info);
}
static struct command_result *param_escaped_string_array(struct command *cmd,
const char *name,
const char *buffer,
const jsmntok_t *tok,
const char ***arr)
{
size_t i;
const jsmntok_t *s;
if (tok->type != JSMN_ARRAY)
return command_fail_badparam(cmd, name, buffer, tok,
"should be an array");
*arr = tal_arr(cmd, const char *, tok->size);
json_for_each_arr(i, s, tok) {
struct command_result *ret;
ret = param_escaped_string(cmd, name, buffer, s, &(*arr)[i]);
if (ret)
return ret;
tal_steal((*arr)[i], *arr);
}
return NULL;
}
static struct command_result *json_bkpr_report(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
struct report_info *info = tal(cmd, struct report_info);
if (!param(cmd, buf, params,
p_req("format", param_report_format, &info->format),
p_opt("headers", param_escaped_string_array, &info->headers),
p_opt_def("escape", param_escape_format, &info->escapes, REPORT_FMT_NONE),
p_opt_def("start_time", param_u64, &info->start_time, 0),
p_opt_def("end_time", param_u64, &info->end_time, SQLITE_MAX_UINT),
NULL))
return command_param_failed();
return refresh_moves(cmd, do_bkpr_report, info);
}
struct list_income_info {
bool *consolidate_fees;
u64 *start_time, *end_time;
};
static struct command_result *do_list_income(struct command *cmd,
struct list_income_info *info)
{
struct json_stream *res;
struct income_event **evs;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
evs = list_income_events(cmd, bkpr, cmd, *info->start_time, *info->end_time,
*info->consolidate_fees);
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_list_income(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
struct list_income_info *info = tal(cmd, struct list_income_info);
if (!param(cmd, buf, params,
p_opt_def("consolidate_fees", param_bool,
&info->consolidate_fees, true),
p_opt_def("start_time", param_u64, &info->start_time, 0),
p_opt_def("end_time", param_u64, &info->end_time, SQLITE_MAX_UINT),
NULL))
return command_param_failed();
return refresh_moves(cmd, do_list_income, info);
}
static struct command_result *do_inspect(struct command *cmd,
char *acct_name)
{
struct json_stream *res;
struct account *acct;
struct fee_sum **fee_sums;
struct txo_set **txos;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
if (!is_channel_account(acct_name))
return command_fail(cmd, PLUGIN_ERROR,
"`inspect` not supported for"
" non-channel accounts");
acct = find_account(bkpr, acct_name);
if (!acct)
return command_fail(cmd, PLUGIN_ERROR,
"Account %s not found",
acct_name);
find_txo_chain(cmd, bkpr, cmd, acct, &txos);
fee_sums = find_account_onchain_fees(cmd, bkpr, acct);
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);
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(res, "fees_paid_msat",
fee_sum->fees_paid);
else
json_add_amount_msat(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];
if (pr->txo) {
if (pr->txo->origin_acct) {
if (!streq(pr->txo->origin_acct, acct->name))
continue;
} else if (!streq(pr->txo->acct_name, acct->name)
&& !is_wallet_account(pr->txo->acct_name))
continue;
} else if (pr->spend
&& !streq(pr->spend->acct_name, acct->name))
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(res, "output_value_msat",
ev->output_value);
json_add_amount_msat(res, "credit_msat",
ev->credit);
json_add_string(res, "currency",
chainparams->lightning_hrp);
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 (!set->pairs[j]->txo) {
json_add_string(res, "account",
ev->acct_name);
json_add_num(res, "outnum",
ev->outpoint.n);
json_add_amount_msat(res,
"output_value_msat",
ev->output_value);
json_add_string(res, "currency",
chainparams->lightning_hrp);
}
json_add_string(res, "spend_tag", ev->tag);
json_add_txid(res, "spending_txid",
ev->spending_txid);
json_add_amount_msat(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);
}
static struct command_result *json_inspect(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
char *acct_name;
if (!param(cmd, buf, params,
p_req("account", param_string, cast_const2(const char **, &acct_name)),
NULL))
return command_param_failed();
return refresh_moves(cmd, do_inspect, acct_name);
}
static void json_add_events(struct json_stream *res,
const struct bkpr *bkpr,
struct channel_event **channel_events,
struct chain_event **chain_events,
struct onchain_fee **onchain_fees)
{
for (size_t i = 0, j = 0, k = 0;
i < tal_count(chain_events)
|| j < tal_count(channel_events)
|| k < tal_count(onchain_fees);
) {
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;
if (chain && chain->timestamp == lowest) {
json_add_chain_event(res, bkpr, chain);
i++;
continue;
}
if (chan && chan->timestamp == lowest) {
json_add_channel_event(res, bkpr, chan);
j++;
continue;
}
json_add_onchain_fee(res, bkpr, fee);
k++;
}
}
struct account_events_info {
const char *acct_name;
struct sha256 *payment_id;
};
static struct command_result *do_account_events(struct command *cmd,
struct account_events_info *info)
{
struct json_stream *res;
struct account *acct;
struct bitcoin_txid *tx_id;
struct channel_event **channel_events;
struct chain_event **chain_events;
struct onchain_fee **onchain_fees;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
if (info->acct_name && info->payment_id != NULL) {
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Can only specify one of "
"{account} or {payment_id}");
}
if (info->acct_name) {
acct = find_account(bkpr, info->acct_name);
if (!acct)
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
"Account '%s' not found",
info->acct_name);
} else
acct = NULL;
if (acct) {
channel_events = account_get_channel_events(cmd, bkpr, cmd, acct);
chain_events = account_get_chain_events(cmd, bkpr, cmd, acct);
onchain_fees = account_get_chain_fees(tmpctx, bkpr, acct->name);
} else if (info->payment_id != NULL) {
channel_events = get_channel_events_by_id(cmd, bkpr, cmd, info->payment_id);
tx_id = tal(cmd, struct bitcoin_txid);
tx_id->shad.sha = *info->payment_id;
reverse_bytes(tx_id->shad.sha.u.u8, sizeof(tx_id->shad.sha.u.u8));
chain_events = find_chain_events_bytxid(cmd, bkpr, cmd, tx_id);
onchain_fees = get_chain_fees_by_txid(cmd, bkpr, tx_id);
} else {
channel_events = list_channel_events(cmd, bkpr, cmd);
chain_events = list_chain_events(cmd, bkpr, cmd);
onchain_fees = list_chain_fees(cmd, bkpr);
}
res = jsonrpc_stream_success(cmd);
json_array_start(res, "events");
json_add_events(res, bkpr, channel_events, chain_events, onchain_fees);
json_array_end(res);
return command_finished(cmd, res);
}
static struct command_result *json_list_account_events(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
struct account_events_info *info = tal(cmd, struct account_events_info);
if (!param(cmd, buf, params,
p_opt("account", param_string, &info->acct_name),
p_opt("payment_id", param_sha256, &info->payment_id),
NULL))
return command_param_failed();
return refresh_moves(cmd, do_account_events, info);
}
struct edit_desc_info {
struct bitcoin_outpoint *outpoint;
const char *new_desc;
};
static struct command_result *do_edit_desc(struct command *cmd,
struct edit_desc_info *info)
{
struct json_stream *res;
struct chain_event **chain_events;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
add_utxo_description(cmd, bkpr, info->outpoint, info->new_desc);
chain_events = get_chain_events_by_outpoint(cmd, bkpr, cmd, info->outpoint);
res = jsonrpc_stream_success(cmd);
json_array_start(res, "updated");
json_add_events(res, bkpr, NULL, chain_events, NULL);
json_array_end(res);
return command_finished(cmd, res);
}
static struct command_result *json_edit_desc_utxo(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
struct edit_desc_info *info = tal(cmd, struct edit_desc_info);
if (!param(cmd, buf, params,
p_req("outpoint", param_outpoint, &info->outpoint),
p_req("description", param_string, &info->new_desc),
NULL))
return command_param_failed();
return refresh_moves(cmd, do_edit_desc, info);
}
struct edit_desc_payment_info {
struct sha256 *identifier;
const char *new_desc;
};
static struct command_result *do_edit_desc_payment(struct command *cmd,
struct edit_desc_payment_info *info)
{
struct json_stream *res;
struct channel_event **channel_events;
struct chain_event **chain_events;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
add_payment_hash_description(cmd, bkpr, info->identifier, info->new_desc);
chain_events = get_chain_events_by_id(cmd, bkpr, cmd, info->identifier);
channel_events = get_channel_events_by_id(cmd, bkpr, cmd, info->identifier);
res = jsonrpc_stream_success(cmd);
json_array_start(res, "updated");
json_add_events(res, bkpr, channel_events, chain_events, NULL);
json_array_end(res);
return command_finished(cmd, res);
}
static struct command_result *json_edit_desc_payment_id(struct command *cmd,
const char *buf,
const jsmntok_t *params)
{
struct edit_desc_payment_info *info = tal(cmd, struct edit_desc_payment_info);
if (!param(cmd, buf, params,
p_req("payment_id", param_sha256, &info->identifier),
p_req("description", param_string, &info->new_desc),
NULL))
return command_param_failed();
return refresh_moves(cmd, do_edit_desc_payment, info);
}
static struct command_result *do_list_balances(struct command *cmd,
void *unused)
{
struct json_stream *res;
struct account **accts;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
res = jsonrpc_stream_success(cmd);
accts = list_accounts(cmd, bkpr);
json_array_start(res, "accounts");
for (size_t i = 0; i < tal_count(accts); i++) {
struct amount_msat credit, debit, balance;
bool has_events;
has_events = account_get_credit_debit(bkpr, cmd,
accts[i]->name,
&credit, &debit);
if (!amount_msat_sub(&balance, credit, debit)) {
plugin_err(cmd->plugin,
"Account balance underflow for account %s (credit %s, debit %s)",
accts[i]->name,
fmt_amount_msat(tmpctx, credit),
fmt_amount_msat(tmpctx, debit));
}
if (is_external_account(accts[i]->name))
continue;
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 < has_events; j++) {
json_object_start(res, NULL);
json_add_amount_msat(res, "balance_msat",
balance);
json_add_string(res, "coin_type",
chainparams->lightning_hrp);
json_object_end(res);
}
json_array_end(res);
json_object_end(res);
}
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)
{
if (!param(cmd, buf, params, NULL))
return command_param_failed();
return refresh_moves(cmd, do_list_balances, NULL);
}
struct new_account_info {
struct account *acct;
struct amount_msat curr_bal;
u32 timestamp;
};
static struct command_result *log_error(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *error,
void *arg UNNEEDED)
{
plugin_log(cmd->plugin, LOG_BROKEN,
"error calling %s: %.*s",
method, json_tok_full_len(error),
json_tok_full(buf, error));
return notification_handled(cmd);
}
static char *do_account_close_checks(struct command *cmd,
struct bkpr *bkpr,
struct chain_event *e,
struct account *acct)
{
struct account *closed_acct;
if (!is_channel_account(acct->name) && e->origin_acct) {
closed_acct = find_account(bkpr, e->origin_acct);
} else if (!is_channel_account(acct->name) && !e->spending_txid) {
const char *acctname;
acctname = find_close_account_name(tmpctx, bkpr, cmd, &e->outpoint.txid);
if (acctname) {
closed_acct = find_account(bkpr, acctname);
} else {
closed_acct = NULL;
}
} else
closed_acct = find_account(bkpr, acct->name);
if (closed_acct && closed_acct->closed_event_db_id) {
u64 closeheight = account_onchain_closeheight(bkpr, cmd, closed_acct);
if (closeheight != 0) {
char *err;
account_update_closeheight(cmd, closed_acct, closeheight);
err = update_channel_onchain_fees(cmd, cmd, bkpr, closed_acct);
if (err) {
return err;
}
}
}
return NULL;
}
static char *fetch_out_desc_invstr(const tal_t *ctx, const char *buf,
const jsmntok_t *tok, char **err)
{
char *bolt, *desc, *fail;
if (!json_scan(ctx, buf, tok, "{bolt11:%}",
JSON_SCAN_TAL(ctx, json_strdup, &bolt))) {
struct bolt11 *bolt11;
const u5 *sigdata;
struct sha256 hash;
bool have_n;
bolt11 = bolt11_decode_nosig(ctx, bolt,
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",
fmt_sha256(ctx,
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_minimal(ctx, bolt, strlen(bolt),
NULL, NULL,
&fail);
if (!bolt12) {
*err = tal_fmt(ctx, "failed to parse"
" bolt12 %s: %s",
bolt, fail);
return NULL;
}
if (bolt12->offer_description)
desc = tal_strndup(ctx,
cast_signed(char *, bolt12->offer_description),
tal_bytelen(bolt12->offer_description));
else
desc = NULL;
} else
desc = NULL;
*err = NULL;
return desc;
}
struct payment_hash_info {
struct refresh_info *rinfo;
struct sha256 payment_hash;
};
static struct command_result *
listinvoices_done(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
struct payment_hash_info *phinfo)
{
size_t i;
const jsmntok_t *inv_arr_tok, *inv_tok;
const char *desc;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
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;
if (!json_scan(cmd, buf, inv_tok, "{description:%}",
JSON_SCAN_TAL(cmd, json_strdup, &desc)))
break;
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) {
add_payment_hash_description(cmd, bkpr, &phinfo->payment_hash,
json_escape_unescape(cmd,
(struct json_escape *)desc));
} else
plugin_log(cmd->plugin, LOG_DBG,
"listinvoices:"
" description/bolt11/bolt12"
" not found (%.*s)",
result->end - result->start, buf + result->start);
return rinfo_one_done(cmd, phinfo->rinfo);
}
static struct command_result *
listsendpays_done(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
struct payment_hash_info *phinfo)
{
size_t i;
const jsmntok_t *pays_arr_tok, *pays_tok;
const char *desc;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
pays_arr_tok = json_get_member(buf, result, "payments");
assert(pays_arr_tok->type == JSMN_ARRAY);
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) {
add_payment_hash_description(cmd, bkpr, &phinfo->payment_hash, desc);
} else
plugin_log(cmd->plugin, LOG_DBG,
"listpays: bolt11/bolt12 not found:"
"(%.*s)",
result->end - result->start, buf + result->start);
return rinfo_one_done(cmd, phinfo->rinfo);
}
static struct command_result *lookup_invoice_desc(struct command *cmd,
struct amount_msat credit,
const struct sha256 *payment_hash,
struct refresh_info *rinfo)
{
struct out_req *req;
struct payment_hash_info *phinfo;
phinfo = tal(cmd, struct payment_hash_info);
phinfo->payment_hash = *payment_hash;
phinfo->rinfo = use_rinfo(rinfo);
if (!amount_msat_is_zero(credit))
req = jsonrpc_request_start(cmd,
"listinvoices",
listinvoices_done,
log_error,
phinfo);
else
req = jsonrpc_request_start(cmd,
"listsendpays",
listsendpays_done,
log_error,
phinfo);
json_add_sha256(req->js, "payment_hash", payment_hash);
return send_outreq(req);
}
struct currency_time {
struct refresh_info *rinfo;
const struct iso4217_name_and_divisor *currency;
u64 timestamp;
};
static struct command_result *currencyrate_ds_done(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
struct refresh_info *rinfo)
{
return rinfo_one_done(cmd, rinfo);
}
static struct command_result *currency_done(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *result,
struct currency_time *ctime)
{
struct bkpr *bkpr = bkpr_of(cmd->plugin);
double rate;
const char *err;
err = json_scan(cmd, buf, result, "{rate:%}",
JSON_SCAN(json_to_double, &rate));
if (err) {
plugin_log(cmd->plugin, LOG_BROKEN,
"Invalid currencyrate return '%.*s': %s",
json_tok_full_len(result),
json_tok_full(buf, result), err);
} else if (bkpr->currency == ctime->currency) {
char *val;
const char **key;
u64 raw_rate = (u64)(rate * ratefactor(bkpr->currency));
u64 ts = ctime->timestamp + 1;
struct currencyrate *crate
= uintmap_before(bkpr->currency_rates, &ts);
if (crate) {
u64 crate_end = ts + crate->duration;
if (crate_end > ctime->timestamp)
goto out;
if (raw_rate == crate->raw_rate
&& crate_end + CURRENCYRATE_TOLERANCE_SECONDS > ctime->timestamp) {
uintmap_del(bkpr->currency_rates, ts);
crate->duration = ctime->timestamp - ts + 1;
} else {
crate = NULL;
}
}
if (!crate) {
ts = ctime->timestamp;
crate = tal(bkpr->currency_rates, struct currencyrate);
crate->raw_rate = raw_rate;
crate->duration = 1;
}
uintmap_add(bkpr->currency_rates, ts, crate);
val = tal_fmt(tmpctx, "%"PRIu64":%u",
crate->raw_rate, crate->duration);
key = mkdatastorekey(tmpctx,
"bookkeeper",
"currencyrate",
ctime->currency->name,
take(tal_fmt(NULL, "%"PRIu64, ts)));
jsonrpc_set_datastore_string(cmd, key, val,
"create-or-replace",
currencyrate_ds_done,
plugin_broken_cb,
use_rinfo(ctime->rinfo));
}
out:
return rinfo_one_done(cmd, ctime->rinfo);
}
static struct command_result *currency_error(struct command *cmd,
const char *method,
const char *buf,
const jsmntok_t *error,
struct currency_time *ctime)
{
struct bkpr *bkpr = bkpr_of(cmd->plugin);
if (!bkpr->warned_currency_fail) {
plugin_log(cmd->plugin, LOG_BROKEN,
"error calling %s: %.*s",
method, json_tok_full_len(error),
json_tok_full(buf, error));
bkpr->warned_currency_fail = true;
}
return rinfo_one_done(cmd, ctime->rinfo);
}
static void lookup_currency(struct command *cmd,
struct bkpr *bkpr,
enum mvt_tag primary_tag,
u64 timestamp,
struct refresh_info *rinfo)
{
struct out_req *req;
struct currency_time *ctime;
u64 now;
if (covering_currencyrate(bkpr, timestamp) != NULL)
return;
now = clock_time().ts.tv_sec;
if (now > timestamp + CURRENCYRATE_TOLERANCE_SECONDS) {
if (!uintmap_empty(bkpr->currency_rates)
&& !bkpr->warned_currency_fail) {
plugin_log(cmd->plugin, LOG_BROKEN,
"Event %s timestamp %"PRIu64" is %"PRIu64" seconds old: too old for current %s currencyrate (only logging first such event: there may be others)",
mvt_tag_str(primary_tag),
timestamp, now - timestamp,
bkpr->currency->name);
bkpr->warned_currency_fail = true;
}
return;
}
ctime = tal(cmd, struct currency_time);
ctime->timestamp = timestamp;
ctime->currency = bkpr->currency;
ctime->rinfo = use_rinfo(rinfo);
req = jsonrpc_request_start(cmd,
"currencyrate",
currency_done,
currency_error,
ctime);
json_add_string(req->js, "currency", bkpr->currency->name);
send_outreq(req);
}
static enum mvt_tag *json_to_tags(const tal_t *ctx, const char *buffer, const jsmntok_t *tok)
{
size_t i;
const jsmntok_t *t;
enum mvt_tag *tags = tal_arr(ctx, enum mvt_tag, tok->size);
json_for_each_arr(i, t, tok) {
if (!json_to_coin_mvt_tag(buffer, t, &tags[i]))
return tal_free(tags);
}
return tags;
}
static void
parse_and_log_chain_move(struct command *cmd,
const char *buf,
const jsmntok_t *chainmove,
struct refresh_info *rinfo)
{
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;
u32 closed_count;
char *acct_name;
const char *err;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
enum mvt_tag tag, *tags;
closed_count = 0;
err = json_scan(tmpctx, buf, chainmove,
"{account_id:%"
",created_index:%"
",credit_msat:%"
",debit_msat:%"
",timestamp:%"
",utxo:%"
",output_msat:%"
",blockheight:%"
",primary_tag:%"
",extra_tags:%"
",output_count?:%"
"}",
JSON_SCAN_TAL(tmpctx, json_strdup, &acct_name),
JSON_SCAN(json_to_u64, &e->db_id),
JSON_SCAN(json_to_msat, &e->credit),
JSON_SCAN(json_to_msat, &e->debit),
JSON_SCAN(json_to_u64, &e->timestamp),
JSON_SCAN(json_to_outpoint, &e->outpoint),
JSON_SCAN(json_to_msat, &e->output_value),
JSON_SCAN(json_to_number, &e->blockheight),
JSON_SCAN(json_to_coin_mvt_tag, &tag),
JSON_SCAN_TAL(tmpctx, json_to_tags, &tags),
JSON_SCAN(json_to_number, &closed_count));
if (err)
plugin_err(cmd->plugin,
"chainmove did"
" not scan %s: %.*s",
err, json_tok_full_len(chainmove),
json_tok_full(buf, chainmove));
err = json_scan(tmpctx, buf, chainmove,
"{spending_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);
err = json_scan(tmpctx, buf, chainmove,
"{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, chainmove,
"{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, chainmove,
"{peer_id:%}",
JSON_SCAN(json_to_node_id, peer_id));
if (err) {
peer_id = tal_free(peer_id);
err = tal_free(err);
}
e->payment_id = tal_steal(e, payment_hash);
e->tag = mvt_tag_str(tag);
e->stealable = false;
e->splice_close = false;
e->foreign = false;
for (size_t i = 0; i < tal_count(tags); i++) {
e->stealable |= tags[i] == MVT_STEALABLE;
e->splice_close |= tags[i] == MVT_SPLICE;
e->foreign |= tags[i] == MVT_FOREIGN;
}
tal_arr_insert(&tags, 0, tag);
if (e->foreign)
plugin_log(cmd->plugin, LOG_DBG,
"Foreign chain event: %s (%s) %s -%s %"PRIu64" %d %s %s",
e->tag, acct_name,
fmt_amount_msat(tmpctx, e->credit),
fmt_amount_msat(tmpctx, e->debit),
e->timestamp, e->blockheight,
fmt_bitcoin_outpoint(tmpctx, &e->outpoint),
e->spending_txid ? fmt_bitcoin_txid(tmpctx, e->spending_txid) : "");
plugin_log(cmd->plugin, LOG_DBG, "coin_move 2 (%s) %s -%s %s %"PRIu64,
e->tag,
fmt_amount_msat(tmpctx, e->credit),
fmt_amount_msat(tmpctx, e->debit),
CHAIN_MOVE, e->timestamp);
acct = find_or_create_account(cmd, bkpr, acct_name);
if (e->origin_acct)
find_or_create_account(cmd, bkpr, e->origin_acct);
assert(e->db_id > bkpr->chainmoves_index);
bkpr->chainmoves_index = e->db_id;
maybe_update_account(cmd, acct, e, tags, closed_count,
peer_id);
err = maybe_update_onchain_fees(cmd, cmd, bkpr,
e->spending_txid ?
e->spending_txid :
&e->outpoint.txid);
if (err)
plugin_err(cmd->plugin,
"Unable to update onchain fees %s",
err);
if (e->spending_txid) {
maybe_closeout_external_deposits(cmd, bkpr, e->spending_txid,
e->blockheight);
}
err = do_account_close_checks(cmd, bkpr, e, acct);
if (err)
plugin_err(cmd->plugin, "%s", err);
if (e->payment_id) {
for (size_t i = 0; i < tal_count(tags); i++) {
if (tags[i] != MVT_INVOICE)
continue;
lookup_invoice_desc(cmd, e->credit,
e->payment_id, rinfo);
break;
}
}
if (bkpr->currency)
lookup_currency(cmd, bkpr, tag, e->timestamp, rinfo);
}
static void
parse_and_log_channel_move(struct command *cmd,
const char *buf,
const jsmntok_t *channelmove,
struct refresh_info *rinfo)
{
struct channel_event *e = tal(cmd, struct channel_event);
struct account *acct;
const char *err;
char *acct_name;
enum mvt_tag tag;
struct bkpr *bkpr = bkpr_of(cmd->plugin);
e->part_id = 0;
e->fees = AMOUNT_MSAT(0);
err = json_scan(tmpctx, buf, channelmove,
"{account_id:%"
",created_index:%"
",credit_msat:%"
",debit_msat:%"
",timestamp:%"
",primary_tag:%"
",part_id?:%"
",fees_msat?:%}",
JSON_SCAN_TAL(tmpctx, json_strdup, &acct_name),
JSON_SCAN(json_to_u64, &e->db_id),
JSON_SCAN(json_to_msat, &e->credit),
JSON_SCAN(json_to_msat, &e->debit),
JSON_SCAN(json_to_u64, &e->timestamp),
JSON_SCAN(json_to_coin_mvt_tag, &tag),
JSON_SCAN(json_to_number, &e->part_id),
JSON_SCAN(json_to_msat, &e->fees));
if (err)
plugin_err(cmd->plugin,
"channelmove did"
" not scan %s: %.*s",
err, json_tok_full_len(channelmove),
json_tok_full(buf, channelmove));
e->tag = mvt_tag_str(tag);
e->payment_id = tal(e, struct sha256);
err = json_scan(tmpctx, buf, channelmove,
"{payment_hash:%}",
JSON_SCAN(json_to_sha256, e->payment_id));
if (err) {
e->payment_id = tal_free(e->payment_id);
err = tal_free(err);
}
plugin_log(cmd->plugin, LOG_DBG, "coin_move 2 (%s) %s -%s %s %"PRIu64,
e->tag,
fmt_amount_msat(tmpctx, e->credit),
fmt_amount_msat(tmpctx, e->debit),
CHANNEL_MOVE, e->timestamp);
acct = find_account(bkpr, acct_name);
if (!acct)
plugin_err(cmd->plugin,
"Received channel event,"
" but no account exists %s",
acct_name);
assert(e->db_id > bkpr->channelmoves_index);
bkpr->channelmoves_index = e->db_id;
if (e->payment_id && tag == MVT_INVOICE) {
if (!amount_msat_is_zero(e->debit))
maybe_record_rebalance(cmd, bkpr, e);
lookup_invoice_desc(cmd, e->credit, e->payment_id, rinfo);
}
if (bkpr->currency)
lookup_currency(cmd, bkpr, tag, e->timestamp, rinfo);
}
static bool json_to_tok(const char *buffer, const jsmntok_t *tok, const jsmntok_t **ret)
{
*ret = tok;
return true;
}
static struct command_result *inject_refresh_done(struct command *notif_cmd,
void *unused)
{
return notification_handled(notif_cmd);
}
static struct command_result *inject_done(struct command *notif_cmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
void *unused)
{
return refresh_moves(notif_cmd, inject_refresh_done, NULL);
}
static struct command_result *json_utxo_deposit(struct command *cmd, const char *buf, const jsmntok_t *params)
{
const char *acct_name, *origin_acct;
struct amount_msat amount;
struct bitcoin_outpoint outpoint;
u64 timestamp;
u32 blockheight;
const jsmntok_t *transfer_from;
const char *err;
struct out_req *req;
transfer_from = NULL;
err = json_scan(tmpctx, buf, params,
"{utxo_deposit:{"
"account:%"
",transfer_from?:%"
",outpoint:%"
",amount_msat:%"
",timestamp:%"
",blockheight:%"
"}}",
JSON_SCAN_TAL(tmpctx, json_strdup, &acct_name),
JSON_SCAN(json_to_tok, &transfer_from),
JSON_SCAN(json_to_outpoint, &outpoint),
JSON_SCAN(json_to_msat, &amount),
JSON_SCAN(json_to_u64, ×tamp),
JSON_SCAN(json_to_u32, &blockheight));
if (err)
plugin_err(cmd->plugin,
"`utxo_deposit` parameters did not scan %s: %.*s",
err, json_tok_full_len(params),
json_tok_full(buf, params));
if (!transfer_from || json_tok_is_null(buf, transfer_from))
origin_acct = NULL;
else
origin_acct = json_strdup(tmpctx, buf, transfer_from);
req = jsonrpc_request_start(cmd, "injectutxodeposit",
inject_done,
plugin_broken_cb,
NULL);
json_add_string(req->js, "account", acct_name);
if (origin_acct)
json_add_string(req->js, "transfer_from", origin_acct);
json_add_outpoint(req->js, "outpoint", &outpoint);
json_add_amount_msat(req->js, "amount_msat", amount);
json_add_u64(req->js, "timestamp", timestamp);
json_add_u32(req->js, "blockheight", blockheight);
return send_outreq(req);
}
static struct command_result *json_utxo_spend(struct command *cmd, const char *buf, const jsmntok_t *params)
{
const char *acct_name;
struct amount_msat amount;
struct bitcoin_txid spending_txid;
struct bitcoin_outpoint outpoint;
u64 timestamp;
u32 blockheight;
const char *err;
struct out_req *req;
err = json_scan(tmpctx, buf, params,
"{utxo_spend:{"
"account:%"
",outpoint:%"
",spending_txid:%"
",amount_msat:%"
",timestamp:%"
",blockheight:%"
"}}",
JSON_SCAN_TAL(tmpctx, json_strdup, &acct_name),
JSON_SCAN(json_to_outpoint, &outpoint),
JSON_SCAN(json_to_txid, &spending_txid),
JSON_SCAN(json_to_msat, &amount),
JSON_SCAN(json_to_u64, ×tamp),
JSON_SCAN(json_to_u32, &blockheight));
if (err)
plugin_err(cmd->plugin,
"`utxo_spend` parameters did not scan %s: %.*s",
err, json_tok_full_len(params),
json_tok_full(buf, params));
req = jsonrpc_request_start(cmd, "injectutxospend",
inject_done,
plugin_broken_cb,
NULL);
json_add_string(req->js, "account", acct_name);
json_add_outpoint(req->js, "outpoint", &outpoint);
json_add_txid(req->js, "spending_txid", &spending_txid);
json_add_amount_msat(req->js, "amount_msat", amount);
json_add_u64(req->js, "timestamp", timestamp);
json_add_u32(req->js, "blockheight", blockheight);
return send_outreq(req);
}
const struct plugin_notification notifs[] = {
{
"utxo_deposit",
json_utxo_deposit,
},
{
"utxo_spend",
json_utxo_spend,
},
};
static const struct plugin_command commands[] = {
{
"bkpr-listbalances",
json_list_balances
},
{
"bkpr-listaccountevents",
json_list_account_events
},
{
"bkpr-inspect",
json_inspect
},
{
"bkpr-listincome",
json_list_income
},
{
"bkpr-dumpincomecsv",
json_dump_income
},
{
"bkpr-channelsapy",
json_channel_apy
},
{
"bkpr-report",
json_bkpr_report,
},
{
"bkpr-editdescriptionbypaymentid",
json_edit_desc_payment_id
},
{
"bkpr-editdescriptionbyoutpoint",
json_edit_desc_utxo
},
};
static bool json_hex_to_be64(const char *buffer, const jsmntok_t *tok,
be64 *val)
{
return hex_decode(buffer + tok->start, tok->end - tok->start,
val, sizeof(*val));
}
static void memleak_scan_currencyrates(struct htable *memtable,
currencymap_t *currency_rates)
{
memleak_scan_uintmap(memtable, currency_rates);
}
static struct command_result *
currency_chainmoves_wait(struct command *auxcmd, void *unused);
static struct command_result *
currency_channelmoves_wait(struct command *auxcmd, void *unused);
static struct command_result *
currency_chainmoves_wait_done(struct command *auxcmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
void *unused)
{
return refresh_moves(auxcmd, currency_chainmoves_wait, NULL);
}
static struct command_result *
currency_channelmoves_wait_done(struct command *auxcmd,
const char *methodname,
const char *buf,
const jsmntok_t *result,
void *unused)
{
return refresh_moves(auxcmd, currency_channelmoves_wait, NULL);
}
static struct command_result *
currency_chainmoves_wait(struct command *auxcmd, void *unused)
{
struct bkpr *bkpr = bkpr_of(auxcmd->plugin);
struct out_req *req;
req = jsonrpc_request_start(auxcmd, "wait",
currency_chainmoves_wait_done,
plugin_broken_cb,
NULL);
json_add_string(req->js, "subsystem", "chainmoves");
json_add_string(req->js, "indexname", "created");
json_add_u64(req->js, "nextvalue", bkpr->chainmoves_index+1);
return send_outreq(req);
}
static struct command_result *
currency_channelmoves_wait(struct command *auxcmd, void *unused)
{
struct bkpr *bkpr = bkpr_of(auxcmd->plugin);
struct out_req *req;
req = jsonrpc_request_start(auxcmd, "wait",
currency_channelmoves_wait_done,
plugin_broken_cb,
NULL);
json_add_string(req->js, "subsystem", "channelmoves");
json_add_string(req->js, "indexname", "created");
json_add_u64(req->js, "nextvalue", bkpr->channelmoves_index+1);
return send_outreq(req);
}
static void start_waiting_for_currency(struct bkpr *bkpr, struct command *cmd)
{
bkpr->currency_cmds = aux_command(cmd);
currency_chainmoves_wait(bkpr->currency_cmds, NULL);
currency_channelmoves_wait(bkpr->currency_cmds, NULL);
}
static const char *init(struct command *init_cmd, const char *b, const jsmntok_t *t)
{
struct plugin *p = init_cmd->plugin;
struct bkpr *bkpr = bkpr_of(p);
be64 index;
bkpr->accounts = init_accounts(bkpr, init_cmd);
bkpr->onchain_fees = init_onchain_fees(bkpr, init_cmd);
bkpr->descriptions = init_descriptions(bkpr, init_cmd);
bkpr->rebalances = init_rebalances(bkpr, init_cmd);
bkpr->blockheights = init_blockheights(bkpr, init_cmd);
bkpr->rinfo = NULL;
find_or_create_account(init_cmd, bkpr, ACCOUNT_NAME_WALLET);
if (rpc_scan_datastore_hex(tmpctx, init_cmd,
mkdatastorekey(tmpctx, "bookkeeper", "channelmoves_index"),
JSON_SCAN(json_hex_to_be64, &index)) == NULL) {
bkpr->channelmoves_index = be64_to_cpu(index);
} else
bkpr->channelmoves_index = 0;
if (rpc_scan_datastore_hex(tmpctx, init_cmd,
mkdatastorekey(tmpctx, "bookkeeper", "chainmoves_index"),
JSON_SCAN(json_hex_to_be64, &index)) == NULL) {
bkpr->chainmoves_index = be64_to_cpu(index);
} else
bkpr->chainmoves_index = 0;
if (bkpr->currency)
start_waiting_for_currency(bkpr, init_cmd);
return NULL;
}
static void load_currencyrates(struct command *cmd,
struct bkpr *bkpr)
{
struct json_out *params;
const jsmntok_t *reply, *ds, *t;
const char *buf;
size_t i;
params = json_out_new(NULL);
json_out_start(params, NULL, '{');
json_out_start(params, "key", '[');
json_out_addstr(params, NULL, "bookkeeper");
json_out_addstr(params, NULL, "currencyrate");
json_out_addstr(params, NULL, bkpr->currency->name);
json_out_end(params, ']');
json_out_end(params, '}');
json_out_finished(params);
reply = jsonrpc_request_sync(tmpctx, cmd, "listdatastore",
take(params), &buf);
ds = json_get_member(buf, reply, "datastore");
json_for_each_arr(i, t, ds) {
const jsmntok_t *data, *keytok = json_get_member(buf, t, "key");
jsmntok_t ratetok, durationtok;
u64 ts;
struct currencyrate *crate;
if (keytok->size != 4)
goto weird;
if (!json_to_u64(buf, keytok + 4, &ts))
goto weird;
data = json_get_member(buf, t, "string");
if (!data)
goto weird;
if (!split_tok(buf, data, ':', &ratetok, &durationtok))
goto weird;
crate = tal(bkpr->currency_rates, struct currencyrate);
if (!json_to_u64(buf, &ratetok, &crate->raw_rate)
|| !json_to_u32(buf, &durationtok, &crate->duration)) {
tal_free(crate);
goto weird;
}
uintmap_add(bkpr->currency_rates, ts, crate);
continue;
weird:
plugin_log(cmd->plugin, LOG_BROKEN,
"Unexpected datastore rate entry '%.*s'",
json_tok_full_len(t),
json_tok_full(buf, t));
}
}
static bool currency_jsonfmt(struct command *cmd,
struct json_stream *js,
const char *fieldname,
struct bkpr *bkpr)
{
if (!bkpr->currency)
return false;
json_add_string(js, fieldname, bkpr->currency->name);
return true;
}
static char *option_currency(struct command *cmd,
const char *arg,
bool check_only,
struct bkpr *bkpr)
{
const struct iso4217_name_and_divisor *newcur;
if (!streq(arg, "")) {
newcur = find_iso4217(arg, strlen(arg));
if (!newcur)
return "unknown ISO4217 code";
} else
newcur = NULL;
if (check_only)
return NULL;
if (bkpr->currency != NULL) {
bkpr->currency_cmds = tal_free(bkpr->currency_cmds);
tal_free(bkpr->currency_rates);
bkpr->currency_rates = tal(bkpr, currencymap_t);
uintmap_init(bkpr->currency_rates);
memleak_add_helper(bkpr->currency_rates, memleak_scan_currencyrates);
bkpr->currency = NULL;
}
if (!newcur)
return NULL;
bkpr->currency = newcur;
bkpr->warned_currency_fail = false;
load_currencyrates(cmd, bkpr);
if (bkpr->accounts)
start_waiting_for_currency(bkpr, cmd);
return NULL;
}
int main(int argc, char *argv[])
{
struct bkpr *bkpr;
setup_locale();
bkpr = tal(NULL, struct bkpr);
bkpr->currency = NULL;
bkpr->currency_rates = tal(bkpr, currencymap_t);
bkpr->accounts = NULL;
uintmap_init(bkpr->currency_rates);
memleak_add_helper(bkpr->currency_rates, memleak_scan_currencyrates);
plugin_main(argv, init, take(bkpr), PLUGIN_STATIC, true, NULL,
commands, ARRAY_SIZE(commands),
notifs, ARRAY_SIZE(notifs),
NULL, 0,
NULL, 0,
plugin_option_dynamic("bkpr-currency",
"string",
"Look up and record this currency on each event",
option_currency, currency_jsonfmt, bkpr),
NULL);
return 0;
}