diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a12ec3..9455574 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased] + +### Added + +- New column for the main channels table: ``PERC_US``: the percentage of funds in the channel that belong to us. + ## [3.3.1] 2024-06-29 ### Added diff --git a/README.md b/README.md index 01f6ec0..d4b29e0 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ You can mix these methods and if you set the same option with different methods, # Options ### Channels table -* ``summars-columns`` Comma-separated list of enabled columns in the channel table. Also dictates order of columns. Valid columns: ``GRAPH_SATS``, ``OUT_SATS``, ``IN_SATS``, ``SCID``, ``MAX_HTLC``, ``FLAG``, ``BASE``, ``PPM``, ``ALIAS``, ``PEER_ID``, ``UPTIME``, ``HTLCS``, ``STATE``. Default columns: ``OUT_SATS``, ``IN_SATS``, ``SCID``, ``MAX_HTLC``, ``FLAG``, ``BASE``, ``PPM``, ``ALIAS``, ``PEER_ID``, ``UPTIME``, ``HTLCS``, ``STATE`` +* ``summars-columns`` Comma-separated list of enabled columns in the channel table. Also dictates order of columns. Valid columns: ``GRAPH_SATS``, ``PERC_US``, ``OUT_SATS``, ``IN_SATS``, ``SCID``, ``MAX_HTLC``, ``FLAG``, ``BASE``, ``PPM``, ``ALIAS``, ``PEER_ID``, ``UPTIME``, ``HTLCS``, ``STATE``. Default columns: ``OUT_SATS``, ``IN_SATS``, ``SCID``, ``MAX_HTLC``, ``FLAG``, ``BASE``, ``PPM``, ``ALIAS``, ``PEER_ID``, ``UPTIME``, ``HTLCS``, ``STATE`` * ``summars-sort-by`` Sort by column name. Use ``-`` before column name to reverse sort. Valid columns are all ``summars-columns`` except for ``GRAPH_SATS``. Default is ``SCID`` * ``summars-exclude-states`` List if excluded channel states. Comma-separated. Valid states are: ``OPENING``, ``AWAIT_LOCK``, ``OK``, ``SHUTTING_DOWN``, ``CLOSINGD_SIGEX``, ``CLOSINGD_DONE``, ``AWAIT_UNILATERAL``, ``FUNDING_SPEND``, ``ONCHAIN``, ``DUAL_OPEN``, ``DUAL_COMITTED``, ``DUAL_COMMIT_RDY``, ``DUAL_AWAIT``, ``AWAIT_SPLICE`` and ``PUBLIC``, ``PRIVATE`` to filter channels by their network visibility, aswell as or `ONLINE,OFFLINE` to filter by connection status. ### Forwards table diff --git a/src/main.rs b/src/main.rs index 22c6163..dd72bc2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,8 +53,9 @@ async fn main() -> Result<(), anyhow::Error> { let opt_columns: StringConfigOption = ConfigOption::new_str_no_default( OPT_COLUMNS, "Enabled columns in the channel table. Available columns are: \ - `GRAPH_SATS,OUT_SATS,IN_SATS,SCID,MAX_HTLC,FLAG,BASE,PPM,ALIAS,PEER_ID,UPTIME,HTLCS,STATE` \ - Default is `OUT_SATS,IN_SATS,SCID,MAX_HTLC,FLAG,BASE,PPM,ALIAS,PEER_ID,UPTIME,HTLCS,STATE`", + `GRAPH_SATS,PERC_US,OUT_SATS,IN_SATS,SCID,MAX_HTLC,FLAG,BASE,PPM,ALIAS,PEER_ID,UPTIME,\ + HTLCS,STATE` Default is `OUT_SATS,IN_SATS,SCID,MAX_HTLC,FLAG,BASE,PPM,ALIAS,PEER_ID,\ + UPTIME,HTLCS,STATE`", ) .dynamic(); let opt_sort_by: StringConfigOption = ConfigOption::new_str_no_default( diff --git a/src/structs.rs b/src/structs.rs index ab9a82c..3085db0 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -55,7 +55,7 @@ impl Config { value: { Summary::FIELD_NAMES_AS_ARRAY .into_iter() - .filter(|t| t != &"graph_sats") + .filter(|t| t != &"graph_sats" && t != &"perc_us") .map(ToString::to_string) .collect::>() }, @@ -231,6 +231,7 @@ pub struct Summary { pub uptime: f64, pub htlcs: usize, pub state: String, + pub perc_us: f64, } #[derive(Debug, Tabled, FieldNamesAsArray, Serialize)] diff --git a/src/tables.rs b/src/tables.rs index 438ac8f..98486b2 100644 --- a/src/tables.rs +++ b/src/tables.rs @@ -1039,6 +1039,7 @@ fn chan_to_summary( uptime: avail * 100.0, htlcs: chan.htlcs.clone().unwrap_or_default().len(), state: statestr.to_string(), + perc_us: (to_us_msat as f64 / total_msat as f64) * 100.0, }) } @@ -1149,6 +1150,21 @@ fn sort_summary(config: &Config, table: &mut [Summary]) { table.sort_by_key(|x| x.state.clone()) } } + col if col.eq("PERC_US") => { + if reverse { + table.sort_by(|x, y| { + y.perc_us + .partial_cmp(&x.perc_us) + .unwrap_or(std::cmp::Ordering::Equal) + }) + } else { + table.sort_by(|x, y| { + x.perc_us + .partial_cmp(&y.perc_us) + .unwrap_or(std::cmp::Ordering::Equal) + }) + } + } _ => { if reverse { table.sort_by_key(|x| Reverse(x.scid_raw)) @@ -1207,6 +1223,7 @@ fn format_summary(config: &Config, sumtable: &mut Table) -> Result<(), Error> { sumtable.with(Modify::new(ByColumnName::new("BASE")).with(Alignment::right())); sumtable.with(Modify::new(ByColumnName::new("PPM")).with(Alignment::right())); sumtable.with(Modify::new(ByColumnName::new("UPTIME")).with(Alignment::right())); + sumtable.with(Modify::new(ByColumnName::new("PERC_US")).with(Alignment::right())); sumtable.with(Modify::new(ByColumnName::new("HTLCS")).with(Alignment::right())); sumtable.with(Modify::new(ByColumnName::new("STATE")).with(Alignment::center())); @@ -1220,6 +1237,16 @@ fn format_summary(config: &Config, sumtable: &mut Table) -> Result<(), Error> { } })), ); + sumtable.with( + Modify::new(ByColumnName::new("PERC_US").not(Rows::first())).with(Format::content(|s| { + let av = s.parse::().unwrap_or(-1.0); + if av < 0.0 { + "N/A".to_string() + } else { + format!("{:.1}%", av) + } + })), + ); sumtable.with( Modify::new(ByColumnName::new("OUT_SATS").not(Rows::first())).with(Format::content(|s| { u64_to_sat_string(config, s.parse::().unwrap()).unwrap() diff --git a/tests/test_summars.py b/tests/test_summars.py index 083c063..6db3bf6 100644 --- a/tests/test_summars.py +++ b/tests/test_summars.py @@ -21,6 +21,7 @@ "UPTIME", "HTLCS", "STATE", + "PERC_US", ] forwards_columns = [ @@ -69,7 +70,7 @@ def test_basic(node_factory, get_plugin): # noqa: F811 assert "avail_out=0.00000000 BTC" in result["result"] assert "avail_in=0.00000000 BTC" in result["result"] - expected_columns = [x for x in columns if x != "GRAPH_SATS"] + expected_columns = [x for x in columns if x != "GRAPH_SATS" and x != "PERC_US"] for column in expected_columns: assert column in result["result"] @@ -168,9 +169,7 @@ def test_options(node_factory, get_plugin): # noqa: F811 assert sats_sent != -1 assert preimage != -1 assert ( - description < destination - and destination < sats_sent - and sats_sent < preimage + description < destination and destination < sats_sent and sats_sent < preimage ) for col in invoice_columns: @@ -205,9 +204,7 @@ def test_options(node_factory, get_plugin): # noqa: F811 assert description != -1 assert label != -1 assert paid_at != -1 - assert ( - sats_received < description and description < label and label < paid_at - ) + assert sats_received < description and description < label and label < paid_at for col in forwards_columns: result = node.rpc.call( @@ -363,19 +360,13 @@ def test_options(node_factory, get_plugin): # noqa: F811 def test_option_errors(node_factory, get_plugin): # noqa: F811 node = node_factory.get_node(options={"plugin": get_plugin}) - with pytest.raises( - RpcError, match="not found in valid summars-columns names" - ): + with pytest.raises(RpcError, match="not found in valid summars-columns names"): node.rpc.call("summars", {"summars-columns": "test"}) with pytest.raises(RpcError, match="Duplicate entry"): node.rpc.call("summars", {"summars-columns": "IN_SATS,IN_SATS"}) - with pytest.raises( - RpcError, match="not found in valid summars-columns names" - ): + with pytest.raises(RpcError, match="not found in valid summars-columns names"): node.rpc.call("summars", {"summars-columns": "PRIVATE"}) - with pytest.raises( - RpcError, match="not found in valid summars-columns names" - ): + with pytest.raises(RpcError, match="not found in valid summars-columns names"): node.rpc.call("summars", {"summars-columns": "OFFLINE"}) with pytest.raises( @@ -383,18 +374,12 @@ def test_option_errors(node_factory, get_plugin): # noqa: F811 ): node.rpc.call("summars", {"summars-forwards-columns": "test"}) with pytest.raises(RpcError, match="Duplicate entry"): - node.rpc.call( - "summars", {"summars-forwards-columns": "in_channel,in_channel"} - ) + node.rpc.call("summars", {"summars-forwards-columns": "in_channel,in_channel"}) - with pytest.raises( - RpcError, match="not found in valid summars-pays-columns names" - ): + with pytest.raises(RpcError, match="not found in valid summars-pays-columns names"): node.rpc.call("summars", {"summars-pays-columns": "test"}) with pytest.raises(RpcError, match="Duplicate entry"): - node.rpc.call( - "summars", {"summars-pays-columns": "description,description"} - ) + node.rpc.call("summars", {"summars-pays-columns": "description,description"}) with pytest.raises( RpcError, match="not found in valid summars-invoices-columns names" @@ -425,9 +410,7 @@ def test_option_errors(node_factory, get_plugin): # noqa: F811 node.rpc.call("summars", {"summars-forwards": -1}) with pytest.raises(RpcError, match="not a valid integer"): - node.rpc.call( - "summars", {"summars-forwards-filter-amount-msat": "TEST"} - ) + node.rpc.call("summars", {"summars-forwards-filter-amount-msat": "TEST"}) with pytest.raises(RpcError, match="not a valid integer"): node.rpc.call("summars", {"summars-forwards-filter-fee-msat": "TEST"}) @@ -448,9 +431,7 @@ def test_option_errors(node_factory, get_plugin): # noqa: F811 node.rpc.call("summars", {"summars-invoices": -1}) with pytest.raises(RpcError, match="not a valid integer"): - node.rpc.call( - "summars", {"summars-invoices-filter-amount-msat": "TEST"} - ) + node.rpc.call("summars", {"summars-invoices-filter-amount-msat": "TEST"}) with pytest.raises(RpcError, match="not a valid string"): node.rpc.call("summars", {"summars-locale": -1}) @@ -539,24 +520,18 @@ def test_setconfig_options(node_factory, get_plugin): # noqa: F811 assert err.value.error["message"] == "summars-utf8 is not a valid boolean!" assert err.value.error["code"] == -32602 assert ( - node.rpc.listconfigs("summars-utf8")["configs"]["summars-utf8"][ - "value_bool" - ] + node.rpc.listconfigs("summars-utf8")["configs"]["summars-utf8"]["value_bool"] != "test" ) node.rpc.setconfig("summars-utf8", False) assert ( - node.rpc.listconfigs("summars-utf8")["configs"]["summars-utf8"][ - "value_bool" - ] + node.rpc.listconfigs("summars-utf8")["configs"]["summars-utf8"]["value_bool"] is False ) def test_chanstates(node_factory, bitcoind, get_plugin): # noqa: F811 - l1, l2, l3 = node_factory.get_nodes( - 3, opts=[{"plugin": get_plugin}, {}, {}] - ) + l1, l2, l3 = node_factory.get_nodes(3, opts=[{"plugin": get_plugin}, {}, {}]) l1.fundwallet(10_000_000) l2.fundwallet(10_000_000) l1.rpc.fundchannel( @@ -621,9 +596,9 @@ def test_chanstates(node_factory, bitcoind, get_plugin): # noqa: F811 l3.stop() wait_for( - lambda: not only_one( - l1.rpc.listpeerchannels(l3.info["id"])["channels"] - )["peer_connected"] + lambda: not only_one(l1.rpc.listpeerchannels(l3.info["id"])["channels"])[ + "peer_connected" + ] ) result = l1.rpc.call("summars", {"summars-exclude-states": "OFFLINE"}) assert "O]" not in result["result"] @@ -636,9 +611,7 @@ def test_chanstates(node_factory, bitcoind, get_plugin): # noqa: F811 l1.rpc.close(chans[0]["short_channel_id"]) wait_for( - lambda: only_one(l1.rpc.listpeerchannels(l2.info["id"])["channels"])[ - "state" - ] + lambda: only_one(l1.rpc.listpeerchannels(l2.info["id"])["channels"])["state"] == "CLOSINGD_COMPLETE" ) result = l1.rpc.call("summars") @@ -653,22 +626,14 @@ def test_flowtables(node_factory, bitcoind, get_plugin): # noqa: F811 l1.rpc.connect(l2.info["id"], "localhost", l2.port) l2.rpc.connect(l3.info["id"], "localhost", l3.port) l1.rpc.connect(l3.info["id"], "localhost", l3.port) - l1.rpc.fundchannel( - l2.info["id"], 1_000_000, push_msat=500_000_000, mindepth=1 - ) - l2.rpc.fundchannel( - l3.info["id"], 1_000_000, push_msat=500_000_000, mindepth=1 - ) + l1.rpc.fundchannel(l2.info["id"], 1_000_000, push_msat=500_000_000, mindepth=1) + l2.rpc.fundchannel(l3.info["id"], 1_000_000, push_msat=500_000_000, mindepth=1) bitcoind.generate_block(6) sync_blockheight(bitcoind, [l1, l2, l3]) - cl1 = l2.rpc.listpeerchannels(l1.info["id"])["channels"][0][ - "short_channel_id" - ] - cl2 = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0][ - "short_channel_id" - ] + cl1 = l2.rpc.listpeerchannels(l1.info["id"])["channels"][0]["short_channel_id"] + cl2 = l2.rpc.listpeerchannels(l3.info["id"])["channels"][0]["short_channel_id"] l2.wait_channel_active(cl1) l2.wait_channel_active(cl2)