Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions contrib/msggen/msggen/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -5160,15 +5160,16 @@
"{currencydebit}: debit amount converted into bkpr-currency",
"{currencycreditdebit}: +credit or -debit (or 0) in bkpr-currency",
"",
"If a field is unavailable, it expands to an empty string.",
"If a field is unavailable, it expands to an empty string (or 0 for credit, debit, fees and creditdebit).",
"",
"You can provide fallback with ?, including more variable:",
" * {outpoint?NONE}",
" * {payment_id?txid: {txid?UNKNOWN}}",
"The first one the outpoint, or NONE if that is not available. ",
"The second prints the payment_id, or if that is not available, the string 'txid: ' followed by the txid, or if that is not available, 'txid: UNKNOWN'.",
"",
"The text after ? is used only if that tag would otherwise be empty.",
"Tags support C-style conditional syntax: {tag[?if-set][:if-not-set]}",
" * if-set: text to use when the tag is present (and non-zero for credit, debit, fees and creditdebit). Default is the tag value itself.",
" * if-not-set: text to use when the tag is absent (or zero for amount fields). Default is empty string (or 0 for amount fields).",
"Either or both parts may be omitted, and each part can itself contain tags. For example:",
" * {outpoint:NONE}: the outpoint value, or 'NONE' if not available",
" * {credit:0.00}: the credit value, or '0.00' if zero",
" * {outpoint?[{outpoint}]:NONE}: '[<value>]' if outpoint is available, or 'NONE' if not",
" * {payment_id:{txid:UNKNOWN}}: the payment_id, or the txid if no payment_id, or 'UNKNOWN' if neither",
"",
"To include a literal {, write {{."
]
Expand Down Expand Up @@ -5226,6 +5227,30 @@
}
}
},
"examples": [
{
"request": {
"id": "example:bkpr-report#1",
"method": "bkpr-report",
"params": {
"headers": [
"Date,Tag,Account,Description,Credit,Debit,BTC/USD,Credit (USD),Debit(USD)"
],
"format": "{localtime},{tag},{account},{description?Invoice description {description}:{outpoint?Onchain output {outpoint}:{txid?Onchain transaction {txid}}}},{credit?+{credit}:0},{debit?-{debit}:0},{currencyrate},${currencycredit},${currencydebit}",
"escape": "csv"
}
},
"response": {
"report": [
"Date,Tag,Account,Description,Credit,Debit,BTC/USD,Credit (USD),Debit(USD)",
"2026-04-02 12:42:03,deposit,wallet,Onchain output 37562cf0b3a5d6783ae02925bcc486bb89afe156abf15e692736e5da9a060018:1,+0.02000000000,0,67140.89,$1342.81,$0.00",
"2026-04-02 12:42:04,invoice,9b3114c8003dc8dedbcf3b7bc9fb04e1575a1112aea33be8369ae9670aff64f4,Invoice description test_bkpr_report_invoice,0,-0.00000123456,67140.89,$0.00,$0.08",
"2026-04-02 12:42:04,onchain_fee,9b3114c8003dc8dedbcf3b7bc9fb04e1575a1112aea33be8369ae9670aff64f4,Onchain transaction f464ff0a67e99a36e83ba3ae12115a57e104fbc97b3bcfdbdec83d00c814319b,0,-0.00004927000,67140.89,$0.00,$3.30"
],
"format-hint": "simple"
}
}
],
"author": [
"Rusty Russell [rusty@rustcorp.com.au](mailto:rusty@rustcorp.com.au) is mainly responsible."
],
Expand Down
41 changes: 33 additions & 8 deletions doc/schemas/bkpr-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,16 @@
"{currencydebit}: debit amount converted into bkpr-currency",
"{currencycreditdebit}: +credit or -debit (or 0) in bkpr-currency",
"",
"If a field is unavailable, it expands to an empty string.",
"If a field is unavailable, it expands to an empty string (or 0 for credit, debit, fees and creditdebit).",
"",
"You can provide fallback with ?, including more variable:",
" * {outpoint?NONE}",
" * {payment_id?txid: {txid?UNKNOWN}}",
"The first one the outpoint, or NONE if that is not available. ",
"The second prints the payment_id, or if that is not available, the string 'txid: ' followed by the txid, or if that is not available, 'txid: UNKNOWN'.",
"",
"The text after ? is used only if that tag would otherwise be empty.",
"Tags support C-style conditional syntax: {tag[?if-set][:if-not-set]}",
" * if-set: text to use when the tag is present (and non-zero for credit, debit, fees and creditdebit). Default is the tag value itself.",
" * if-not-set: text to use when the tag is absent (or zero for amount fields). Default is empty string (or 0 for amount fields).",
"Either or both parts may be omitted, and each part can itself contain tags. For example:",
" * {outpoint:NONE}: the outpoint value, or 'NONE' if not available",
" * {credit:0.00}: the credit value, or '0.00' if zero",
" * {outpoint?[{outpoint}]:NONE}: '[<value>]' if outpoint is available, or 'NONE' if not",
" * {payment_id:{txid:UNKNOWN}}: the payment_id, or the txid if no payment_id, or 'UNKNOWN' if neither",
"",
"To include a literal {, write {{."
]
Expand Down Expand Up @@ -112,6 +113,30 @@
}
}
},
"examples": [
{
"request": {
"id": "example:bkpr-report#1",
"method": "bkpr-report",
"params": {
"headers": [
"Date,Tag,Account,Description,Credit,Debit,BTC/USD,Credit (USD),Debit(USD)"
],
"format": "{localtime},{tag},{account},{description?Invoice description {description}:{outpoint?Onchain output {outpoint}:{txid?Onchain transaction {txid}}}},{credit?+{credit}:0},{debit?-{debit}:0},{currencyrate},${currencycredit},${currencydebit}",
"escape": "csv"
}
},
"response": {
"report": [
"Date,Tag,Account,Description,Credit,Debit,BTC/USD,Credit (USD),Debit(USD)",
"2026-04-02 12:42:03,deposit,wallet,Onchain output 37562cf0b3a5d6783ae02925bcc486bb89afe156abf15e692736e5da9a060018:1,+0.02000000000,0,67140.89,$1342.81,$0.00",
"2026-04-02 12:42:04,invoice,9b3114c8003dc8dedbcf3b7bc9fb04e1575a1112aea33be8369ae9670aff64f4,Invoice description test_bkpr_report_invoice,0,-0.00000123456,67140.89,$0.00,$0.08",
"2026-04-02 12:42:04,onchain_fee,9b3114c8003dc8dedbcf3b7bc9fb04e1575a1112aea33be8369ae9670aff64f4,Onchain transaction f464ff0a67e99a36e83ba3ae12115a57e104fbc97b3bcfdbdec83d00c814319b,0,-0.00004927000,67140.89,$0.00,$3.30"
],
"format-hint": "simple"
}
}
],
"author": [
"Rusty Russell [rusty@rustcorp.com.au](mailto:rusty@rustcorp.com.au) is mainly responsible."
],
Expand Down
87 changes: 66 additions & 21 deletions plugins/bkpr/report.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
#include <plugins/bkpr/report.h>
#include <plugins/libplugin.h>

/* This is a zero, but treated specially if tested with ? */
static const char ZERO_AMOUNT[] = "0";

static const char *report_fmt_acct_name(const tal_t *ctx UNNEEDED,
const struct bkpr *bkpr UNNEEDED,
const struct income_event *e)
Expand All @@ -40,20 +43,26 @@ static const char *report_fmt_credit(const tal_t *ctx,
const struct bkpr *bkpr UNNEEDED,
const struct income_event *e)
{
if (amount_msat_is_zero(e->credit))
return ZERO_AMOUNT;
return fmt_amount_msat_btc(ctx, e->credit, false);
}

static const char *report_fmt_debit(const tal_t *ctx,
const struct bkpr *bkpr UNNEEDED,
const struct income_event *e)
{
if (amount_msat_is_zero(e->debit))
return ZERO_AMOUNT;
return fmt_amount_msat_btc(ctx, e->debit, false);
}

static const char *report_fmt_fees(const tal_t *ctx,
const struct bkpr *bkpr UNNEEDED,
const struct income_event *e)
{
if (amount_msat_is_zero(e->fees))
return ZERO_AMOUNT;
return fmt_amount_msat_btc(ctx, e->fees, false);
}

Expand Down Expand Up @@ -134,7 +143,7 @@ static const char *report_fmt_credit_debit(const tal_t *ctx,
if (!amount_msat_is_zero(e->debit))
return tal_fmt(ctx, "-%s",
fmt_amount_msat_btc(tmpctx, e->debit, false));
return "0";
return ZERO_AMOUNT;
}

static const char *report_fmt_currency_credit(const tal_t *ctx,
Expand Down Expand Up @@ -211,8 +220,10 @@ struct report_format {
const struct bkpr *bkpr,
const struct income_event *e);
const char **str;
/* If fmt returns NULL, evaluate these instead. */
struct report_format **alt;
/* If fmt returns non-NULL (and non-ZERO_AMOUNT), evaluate these instead of the value. */
struct report_format **ifset;
/* If fmt returns NULL (or ZERO_AMOUNT when either ifset/ifnotset is non-NULL), evaluate these. */
struct report_format **ifnotset;
};

static void add_literal(struct report_format *f,
Expand All @@ -222,15 +233,18 @@ static void add_literal(struct report_format *f,
tal_arr_expand(&f->fmt, NULL);
tal_arr_expand(&f->str,
tal_strndup(f->str, *start, end - *start));
tal_arr_expand(&f->alt, NULL);
tal_arr_expand(&f->ifset, NULL);
tal_arr_expand(&f->ifnotset, NULL);
*start = end;
}
}

/* alt_term is a secondary loop terminator (in addition to term); '\0' means none. */
static struct report_format *
parse_report_format(const tal_t *ctx,
const char **start,
char term,
char alt_term,
const char **err)
{
const char *p;
Expand All @@ -239,11 +253,12 @@ parse_report_format(const tal_t *ctx,
f = tal(ctx, struct report_format);
f->fmt = tal_arr(f, typeof(*f->fmt), 0);
f->str = tal_arr(f, const char *, 0);
f->alt = tal_arr(f, struct report_format *, 0);
f->ifset = tal_arr(f, struct report_format *, 0);
f->ifnotset = tal_arr(f, struct report_format *, 0);

p = *start;
while (*p != term) {
struct report_format *alt;
while (*p != term && !(alt_term && *p == alt_term)) {
struct report_format *ifset, *ifnotset;
const struct report_tag *rt;

if (*p == '\0') {
Expand All @@ -264,7 +279,8 @@ parse_report_format(const tal_t *ctx,
lit = tal_strcat(tmpctx, take(lit), "{");
tal_arr_expand(&f->fmt, NULL);
tal_arr_expand(&f->str, lit);
tal_arr_expand(&f->alt, NULL);
tal_arr_expand(&f->ifset, NULL);
tal_arr_expand(&f->ifnotset, NULL);
p += 2;
*start = p;
continue;
Expand All @@ -273,7 +289,7 @@ parse_report_format(const tal_t *ctx,
/* Emit preceding literal, if any. */
add_literal(f, start, p);

const char *endtag = p + 1 + strcspn(p+1, "?}");
const char *endtag = p + 1 + strcspn(p+1, "?:}");
if (*endtag == '\0') {
*err = tal_fmt(ctx, "Unterminated tag %s", p + 1);
return tal_free(f);
Expand All @@ -287,25 +303,45 @@ parse_report_format(const tal_t *ctx,
return tal_free(f);
}

ifset = ifnotset = NULL;
if (*endtag == '?') {
/* Parse if-set, which ends at ':' or '}' */
*start = endtag + 1;
ifset = parse_report_format(f, start, '}', ':', err);
if (!ifset) {
tal_steal(ctx, *err);
return tal_free(f);
}
if (**start == ':') {
/* Parse if-not-set */
(*start)++;
ifnotset = parse_report_format(f, start, '}', '\0', err);
if (!ifnotset) {
tal_steal(ctx, *err);
return tal_free(f);
}
}
/* Consume final '}' */
(*start)++;
} else if (*endtag == ':') {
/* Only if-not-set */
*start = endtag + 1;
alt = parse_report_format(f, start, '}', err);
if (!alt) {
/* Steal error upwards! */
ifnotset = parse_report_format(f, start, '}', '\0', err);
if (!ifnotset) {
tal_steal(ctx, *err);
return tal_free(f);
}
/* Consume final } */
/* Consume final '}' */
(*start)++;
} else {
assert(*endtag == '}');
alt = NULL;
*start = endtag + 1;
}

tal_arr_expand(&f->fmt, rt->fmt);
tal_arr_expand(&f->str, NULL);
tal_arr_expand(&f->alt, alt);
tal_arr_expand(&f->ifset, ifset);
tal_arr_expand(&f->ifnotset, ifnotset);

p = *start;
}
Expand All @@ -327,7 +363,7 @@ struct command_result *param_report_format(struct command *cmd,
if (ret)
return ret;

*format = parse_report_format(cmd, &start, '\0', &err);
*format = parse_report_format(cmd, &start, '\0', '\0', &err);
if (!*format)
return command_fail_badparam(cmd, name, buffer, tok, err);

Expand Down Expand Up @@ -411,15 +447,24 @@ static char *format_event(const tal_t *ctx,
}

v = fmt->fmt[i](tmpctx, bkpr, e);
/* Treat ZERO_AMOUNT as absent when there are conditionals. */
if (v == ZERO_AMOUNT && (fmt->ifset[i] || fmt->ifnotset[i]))
v = NULL;

if (v) {
v = escape_value(tmpctx, v, esc);
out = tal_strcat(ctx, take(out), v);
if (fmt->ifset[i]) {
out = tal_strcat(ctx, take(out),
format_event(tmpctx, fmt->ifset[i], esc, bkpr, e));
} else {
out = tal_strcat(ctx, take(out),
escape_value(tmpctx, v, esc));
}
continue;
}

if (fmt->alt[i]) {
char *alt = format_event(tmpctx, fmt->alt[i], esc, bkpr, e);
out = tal_strcat(ctx, take(out), alt);
if (fmt->ifnotset[i]) {
out = tal_strcat(ctx, take(out),
format_event(tmpctx, fmt->ifnotset[i], esc, bkpr, e));
}
}

Expand Down
Loading
Loading