diff --git a/common/coin_mvt.c b/common/coin_mvt.c index 85f999057..5949f1342 100644 --- a/common/coin_mvt.c +++ b/common/coin_mvt.c @@ -61,7 +61,8 @@ struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx, u64 *part_id, struct amount_msat amount, enum mvt_tag *tags STEALS, - bool is_credit) + bool is_credit, + struct amount_msat fees) { struct channel_coin_mvt *mvt = tal(ctx, struct channel_coin_mvt); @@ -78,6 +79,8 @@ struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx, mvt->credit = AMOUNT_MSAT(0); } + mvt->fees = fees; + return mvt; } @@ -326,7 +329,8 @@ struct channel_coin_mvt *new_coin_pushed(const tal_t *ctx, return new_channel_coin_mvt(ctx, cid, empty_hash, NULL, amount, - new_tag_arr(ctx, PUSHED), false); + new_tag_arr(ctx, PUSHED), false, + AMOUNT_MSAT(0)); } struct coin_mvt *finalize_chain_mvt(const tal_t *ctx, @@ -351,6 +355,7 @@ struct coin_mvt *finalize_chain_mvt(const tal_t *ctx, mvt->output_val = tal(mvt, struct amount_sat); *mvt->output_val = chain_mvt->output_val; + mvt->fees = NULL; mvt->timestamp = timestamp; mvt->blockheight = chain_mvt->blockheight; @@ -379,6 +384,8 @@ struct coin_mvt *finalize_channel_mvt(const tal_t *ctx, mvt->credit = chan_mvt->credit; mvt->debit = chan_mvt->debit; mvt->output_val = NULL; + mvt->fees = tal(mvt, struct amount_msat); + *mvt->fees = chan_mvt->fees; mvt->timestamp = timestamp; /* channel movements don't have a blockheight */ mvt->blockheight = 0; diff --git a/common/coin_mvt.h b/common/coin_mvt.h index e0f85df3a..9dfcc80c1 100644 --- a/common/coin_mvt.h +++ b/common/coin_mvt.h @@ -58,6 +58,8 @@ struct channel_coin_mvt { struct amount_msat credit; struct amount_msat debit; + /* Fees collected (or paid) on this mvt */ + struct amount_msat fees; }; struct chain_coin_mvt { @@ -114,6 +116,9 @@ struct coin_mvt { * our credit/debit amount, eg channel opens */ struct amount_sat *output_val; + /* Amount of fees collected/paid by channel mvt */ + struct amount_msat *fees; + u32 timestamp; u32 blockheight; @@ -133,7 +138,8 @@ struct channel_coin_mvt *new_channel_coin_mvt(const tal_t *ctx, u64 *part_id, struct amount_msat amount, enum mvt_tag *tags STEALS, - bool is_credit); + bool is_credit, + struct amount_msat fees); struct chain_coin_mvt *new_onchaind_withdraw(const tal_t *ctx, const struct bitcoin_outpoint *outpoint, diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index 79dfc358f..76ae30f9e 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -708,6 +708,8 @@ i.e. only definitively resolved HTLCs or confirmed bitcoin transactions. "part_id": 0, // (`channel_mvt` type only, mandatory) "credit":"2000000000msat", "debit":"0msat", + "output_value": "2000000000msat", // ('chain_mvt' only) + "fees": "382msat", // ('channel_mvt' only, optional) "tags": ["deposit"], "blockheight":102, // (May be null) "timestamp":1585948198, @@ -747,6 +749,16 @@ multiple times. `channel_mvt` only `credit` and `debit` are millisatoshi denominated amounts of the fund movement. A 'credit' is funds deposited into an account; a `debit` is funds withdrawn. +`output_value` is the total value of the on-chain UTXO. Note that for +channel opens/closes the total output value will not necessarily correspond +to the amount that's credited/debited. + +`fees` is an HTLC annotation for the amount of fees either paid or +earned. For "invoice" tagged events, the fees are the total fees +paid to send that payment. The end amount can be found by subtracting +the total fees from the `debited` amount. For "routed" tagged events, +both the debit/credit contain fees. Technically routed debits are the +'fee generating' event, however we include them on routed credits as well. `tag` is a movement descriptor. Current tags are as follows: - `deposit`: funds deposited diff --git a/lightningd/coin_mvts.c b/lightningd/coin_mvts.c index 968a014db..a29042cf6 100644 --- a/lightningd/coin_mvts.c +++ b/lightningd/coin_mvts.c @@ -1,4 +1,5 @@ #include "config.h" +#include #include #include #include @@ -33,17 +34,26 @@ struct channel_coin_mvt *new_channel_mvt_invoice_hin(const tal_t *ctx, return new_channel_coin_mvt(ctx, &channel->cid, hin->payment_hash, NULL, hin->msat, new_tag_arr(ctx, INVOICE), - true); + true, AMOUNT_MSAT(0)); } struct channel_coin_mvt *new_channel_mvt_routed_hin(const tal_t *ctx, struct htlc_in *hin, struct channel *channel) { + struct amount_msat fees_collected; + + if (!hin->payload) + return NULL; + + if (!amount_msat_sub(&fees_collected, hin->msat, + hin->payload->amt_to_forward)) + return NULL; + return new_channel_coin_mvt(ctx, &channel->cid, hin->payment_hash, NULL, hin->msat, new_tag_arr(ctx, ROUTED), - true); + true, fees_collected); } struct channel_coin_mvt *new_channel_mvt_invoice_hout(const tal_t *ctx, @@ -53,15 +63,25 @@ struct channel_coin_mvt *new_channel_mvt_invoice_hout(const tal_t *ctx, return new_channel_coin_mvt(ctx, &channel->cid, hout->payment_hash, &hout->partid, hout->msat, new_tag_arr(ctx, INVOICE), - false); + false, AMOUNT_MSAT(0)); } struct channel_coin_mvt *new_channel_mvt_routed_hout(const tal_t *ctx, struct htlc_out *hout, struct channel *channel) { + struct amount_msat fees_collected; + + if (!hout->in) + return NULL; + + if (!amount_msat_sub(&fees_collected, hout->in->msat, + hout->msat)) + return NULL; + return new_channel_coin_mvt(ctx, &channel->cid, hout->payment_hash, NULL, hout->msat, new_tag_arr(ctx, ROUTED), - false); + false, + fees_collected); } diff --git a/lightningd/notification.c b/lightningd/notification.c index c5033eaf2..239fbfabf 100644 --- a/lightningd/notification.c +++ b/lightningd/notification.c @@ -474,6 +474,9 @@ static void coin_movement_notification_serialize(struct json_stream *stream, if (mvt->output_val) json_add_amount_sat_only(stream, "output_value", *mvt->output_val); + if (mvt->fees) + json_add_amount_msat_only(stream, "fees", + *mvt->fees); json_array_start(stream, "tags"); for (size_t i = 0; i < tal_count(mvt->tags); i++) diff --git a/lightningd/peer_htlcs.c b/lightningd/peer_htlcs.c index 43c068243..70880689a 100644 --- a/lightningd/peer_htlcs.c +++ b/lightningd/peer_htlcs.c @@ -1544,12 +1544,17 @@ static void remove_htlc_in(struct channel *channel, struct htlc_in *hin) channel->msat_to_us_max = channel->our_msat; /* Coins have definitively moved, log a movement */ - if (hin->we_filled) + if (hin->we_filled && *hin->we_filled) mvt = new_channel_mvt_invoice_hin(hin, hin, channel); else mvt = new_channel_mvt_routed_hin(hin, hin, channel); - notify_channel_mvt(channel->peer->ld, mvt); + if (!mvt) + log_broken(channel->log, + "Unable to calculate fees collected." + " Not logging an inbound HTLC"); + else + notify_channel_mvt(channel->peer->ld, mvt); } tal_free(hin); @@ -1597,7 +1602,13 @@ static void remove_htlc_out(struct channel *channel, struct htlc_out *hout) else mvt = new_channel_mvt_routed_hout(hout, hout, channel); - notify_channel_mvt(channel->peer->ld, mvt); + + if (!mvt) + log_broken(channel->log, + "Unable to calculate fees collected." + " Not logging an outbound HTLC"); + else + notify_channel_mvt(channel->peer->ld, mvt); } tal_free(hout); diff --git a/tests/test_plugin.py b/tests/test_plugin.py index 7de9d31b6..58afbdbf5 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -1888,17 +1888,17 @@ def test_coin_movement_notices(node_factory, bitcoind, chainparams): l1_l2_mvts = [ {'type': 'chain_mvt', 'credit': 0, 'debit': 0, 'tags': ['channel_open']}, - {'type': 'channel_mvt', 'credit': 100001001, 'debit': 0, 'tags': ['routed']}, - {'type': 'channel_mvt', 'credit': 0, 'debit': 50000000, 'tags': ['routed']}, - {'type': 'channel_mvt', 'credit': 100000000, 'debit': 0, 'tags': ['invoice']}, - {'type': 'channel_mvt', 'credit': 0, 'debit': 50000000, 'tags': ['invoice']}, + {'type': 'channel_mvt', 'credit': 100001001, 'debit': 0, 'tags': ['routed'], 'fees': '1001msat'}, + {'type': 'channel_mvt', 'credit': 0, 'debit': 50000000, 'tags': ['routed'], 'fees': '501msat'}, + {'type': 'channel_mvt', 'credit': 100000000, 'debit': 0, 'tags': ['invoice'], 'fees': '0msat'}, + {'type': 'channel_mvt', 'credit': 0, 'debit': 50000000, 'tags': ['invoice'], 'fees': '0msat'}, {'type': 'chain_mvt', 'credit': 0, 'debit': 100001001, 'tags': ['channel_close']}, ] l2_l3_mvts = [ {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['channel_open', 'opener']}, - {'type': 'channel_mvt', 'credit': 0, 'debit': 100000000, 'tags': ['routed']}, - {'type': 'channel_mvt', 'credit': 50000501, 'debit': 0, 'tags': ['routed']}, + {'type': 'channel_mvt', 'credit': 0, 'debit': 100000000, 'tags': ['routed'], 'fees': '1001msat'}, + {'type': 'channel_mvt', 'credit': 50000501, 'debit': 0, 'tags': ['routed'], 'fees': '501msat'}, {'type': 'chain_mvt', 'credit': 0, 'debit': 950000501, 'tags': ['channel_close']}, ] diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 812b8654f..4aaf7557d 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -850,26 +850,26 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams): l1.rpc.signpsbt(invalid_psbt) wallet_coin_mvts = [ - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tag': 'deposit'}, - {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, - {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, - {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, - {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, - {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, - {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, - {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, - {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tag': 'withdrawal'}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 1000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tags': ['withdrawal']}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tags': ['withdrawal']}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tags': ['withdrawal']}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tags': ['withdrawal']}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tags': ['withdrawal']}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tags': ['withdrawal']}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tags': ['withdrawal']}, + {'type': 'chain_mvt', 'credit': 0, 'debit': 1000000000, 'tags': ['withdrawal']}, ] check_coin_moves(l1, 'wallet', wallet_coin_mvts, chainparams) diff --git a/tests/utils.py b/tests/utils.py index def472b0e..528090e67 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -79,6 +79,13 @@ def move_matches(exp, mv): return False if mv['tags'] != exp['tags']: return False + if 'fees' in exp: + if 'fees' not in mv: + return False + if mv['fees'] != exp['fees']: + return False + elif 'fees' in mv: + return False return True @@ -94,11 +101,12 @@ def check_coin_moves(n, account_id, expected_moves, chainparams): node_id = n.info['id'] acct_moves = [m for m in moves if m['account_id'] == account_id] for mv in acct_moves: - print("{{'type': '{}', 'credit': {}, 'debit': {}, 'tags': '{}'}}," + print("{{'type': '{}', 'credit': {}, 'debit': {}, 'tags': '{}' , ['fees'?: '{}']}}," .format(mv['type'], Millisatoshi(mv['credit']).millisatoshis, Millisatoshi(mv['debit']).millisatoshis, - mv['tags'])) + mv['tags'], + mv['fees'] if 'fees' in mv else '')) assert mv['version'] == 2 assert mv['node_id'] == node_id assert mv['timestamp'] > 0