From 3dc88a1e43e2567887a419b2af63aa8178f4aa07 Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Sat, 17 Aug 2024 10:34:32 -0700 Subject: [PATCH] contrib: add clboss-earnings-history and clboss-recent-earnings scripts --- .gitignore | 2 +- contrib/clboss-earnings-history | 125 +++++++++++++++++++++++ contrib/clboss-recent-earnings | 172 ++++++++++++++++++++++++++++++++ contrib/clboss/__init__.py | 0 contrib/clboss/alias_cache.py | 42 ++++++++ 5 files changed, 340 insertions(+), 1 deletion(-) create mode 100755 contrib/clboss-earnings-history create mode 100755 contrib/clboss-recent-earnings create mode 100644 contrib/clboss/__init__.py create mode 100644 contrib/clboss/alias_cache.py diff --git a/.gitignore b/.gitignore index a12905749..b6be42fe7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -clboss +/clboss create-tarball dev-boltz-api dev-boltz diff --git a/contrib/clboss-earnings-history b/contrib/clboss-earnings-history new file mode 100755 index 000000000..598307bf1 --- /dev/null +++ b/contrib/clboss-earnings-history @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 + +import subprocess +import argparse +import json +from datetime import datetime +from tabulate import tabulate + +def run_lightning_cli_command(network_option, command, *args): + try: + result = subprocess.run(['lightning-cli', network_option, command, *args], capture_output=True, text=True, check=True) + return json.loads(result.stdout) + except subprocess.CalledProcessError as e: + print(f"Command '{command}' failed with error: {e}") + except json.JSONDecodeError as e: + print(f"Failed to parse JSON from command '{command}': {e}") + return None + +def format_bucket_time(bucket_time): + if bucket_time == 0: + return "Legacy" + else: + return datetime.utcfromtimestamp(bucket_time).strftime('%Y-%m-%d') + +def main(): + parser = argparse.ArgumentParser(description="Run lightning-cli with specified network") + parser.add_argument('--mainnet', action='store_true', help='Run on mainnet') + parser.add_argument('--testnet', action='store_true', help='Run on testnet') + parser.add_argument('nodeid', nargs='?', help='The node ID to pass to clboss-earnings-history (optional)') + + args = parser.parse_args() + + if args.testnet: + network_option = '--testnet' + else: + network_option = '--mainnet' # Default to mainnet if no option is specified + + if args.nodeid: + earnings_data = run_lightning_cli_command(network_option, 'clboss-earnings-history', args.nodeid) + else: + earnings_data = run_lightning_cli_command(network_option, 'clboss-earnings-history') + + # Initialize totals + total_forwarded = 0 + total_earnings = 0 + total_rebalanced = 0 + total_expense = 0 + total_net_earnings = 0 + + # Process and format data + rows = [] + for entry in earnings_data['history']: + if args.nodeid: + # Sum "in" and "out" values + earnings = entry['in_earnings'] + entry['out_earnings'] + forwarded = entry['in_forwarded'] + entry['out_forwarded'] + expense = entry['in_expenditures'] + entry['out_expenditures'] + rebalanced = entry['in_rebalanced'] + entry['out_rebalanced'] + else: + # Just use the in values, they are symetrical + earnings = entry['in_earnings'] + forwarded = entry['in_forwarded'] + expense = entry['in_expenditures'] + rebalanced = entry['in_rebalanced'] + + # Calculate rates with checks for division by zero + forwarded_rate = (earnings / forwarded) * 1_000_000 if forwarded != 0 else 0 + rebalance_rate = (expense / rebalanced) * 1_000_000 if rebalanced != 0 else 0 + net_earnings = earnings - expense + + # Update totals + total_forwarded += forwarded + total_earnings += earnings + total_rebalanced += rebalanced + total_expense += expense + total_net_earnings += net_earnings + + rows.append([ + format_bucket_time(entry['bucket_time']), + f"{forwarded:,}".replace(',', '_'), + f"{forwarded_rate:,.0f}", + f"{earnings:,}".replace(',', '_'), + f"{rebalanced:,}".replace(',', '_'), + f"{rebalance_rate:,.0f}", + f"{expense:,}".replace(',', '_'), + f"{int(net_earnings):,}".replace(',', '_') + ]) + + # Calculate total rates + total_forwarded_rate = (total_earnings / total_forwarded) * 1_000_000 if total_forwarded != 0 else 0 + total_rebalance_rate = (total_expense / total_rebalanced) * 1_000_000 if total_rebalanced != 0 else 0 + + # Add a separator row + separator_row = ["-" * len(header) for header in ["Date", "Forwarded", "Rate", "Earnings", "Rebalanced", "Rate", "Expense", "Net Earnings"]] + rows.append(separator_row) + + # Append the total row + rows.append([ + "TOTAL", + f"{total_forwarded:,}".replace(',', '_'), + # misleading because legacy: f"{total_forwarded_rate:,.0f}", + f"", + f"{total_earnings:,}".replace(',', '_'), + f"{total_rebalanced:,}".replace(',', '_'), + # misleading because legacy: f"{total_rebalance_rate:,.0f}", + f"", + f"{total_expense:,}".replace(',', '_'), + f"{int(total_net_earnings):,}".replace(',', '_') + ]) + + headers = [ + "Date", + "Forwarded", + "Rate", + "Earnings", + "Rebalanced", + "Rate", + "Expense", + "Net Earnings" + ] + + print(tabulate(rows, headers=headers, tablefmt="pretty", stralign="right", numalign="right")) + +if __name__ == "__main__": + main() diff --git a/contrib/clboss-recent-earnings b/contrib/clboss-recent-earnings new file mode 100755 index 000000000..464cae1c8 --- /dev/null +++ b/contrib/clboss-recent-earnings @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 + +import os +import subprocess +import argparse +import json +from tabulate import tabulate +from clboss.alias_cache import lookup_alias + +def run_lightning_cli_command(network_option, command, *args): + try: + result = subprocess.run(['lightning-cli', network_option, command, *args], capture_output=True, text=True, check=True) + return json.loads(result.stdout) + except subprocess.CalledProcessError as e: + print(f"Command '{command}' failed with error: {e}") + except json.JSONDecodeError as e: + print(f"Failed to parse JSON from command '{command}': {e}") + return None + +def calculate_net_earnings(data, network_option): + rows = [] + + # Initialize totals + total_net_earnings = 0 + total_in_earnings = 0 + total_in_forwarded = 0 + total_in_expenditures = 0 + total_in_rebalanced = 0 + total_out_earnings = 0 + total_out_forwarded = 0 + total_out_expenditures = 0 + total_out_rebalanced = 0 + + for node, stats in data['recent'].items(): + in_earnings = stats['in_earnings'] + in_forwarded = stats['in_forwarded'] + in_expenditures = stats['in_expenditures'] + in_rebalanced = stats['in_rebalanced'] + + out_earnings = stats['out_earnings'] + out_forwarded = stats['out_forwarded'] + out_expenditures = stats['out_expenditures'] + out_rebalanced = stats['out_rebalanced'] + + # Skip rows where all values are zero + if ( + in_earnings == 0 and in_forwarded == 0 and in_expenditures == 0 and in_rebalanced == 0 and + out_earnings == 0 and out_forwarded == 0 and out_expenditures == 0 and out_rebalanced == 0 + ): + continue + alias = lookup_alias(run_lightning_cli_command, network_option, node) + in_rate = (in_earnings / in_forwarded) * 1_000_000 if in_forwarded != 0 else 0 + in_rebalance_rate = (in_expenditures / in_rebalanced) * 1_000_000 if in_rebalanced != 0 else 0 + out_rate = (out_earnings / out_forwarded) * 1_000_000 if out_forwarded != 0 else 0 + out_rebalance_rate = (out_expenditures / out_rebalanced) * 1_000_000 if out_rebalanced != 0 else 0 + + net_earnings = in_earnings - in_expenditures + out_earnings - out_expenditures + + # Update totals + total_net_earnings += net_earnings + total_in_earnings += in_earnings + total_in_forwarded += in_forwarded + total_in_expenditures += in_expenditures + total_in_rebalanced += in_rebalanced + total_out_earnings += out_earnings + total_out_forwarded += out_forwarded + total_out_expenditures += out_expenditures + total_out_rebalanced += out_rebalanced + + avg_in_earnings_rate = (total_in_earnings / total_in_forwarded) * 1_000_000 if total_in_forwarded != 0 else 0 + avg_out_earnings_rate = (total_out_earnings / total_out_forwarded) * 1_000_000 if total_out_forwarded != 0 else 0 + avg_in_expenditures_rate = (total_in_expenditures / total_in_rebalanced) * 1_000_000 if total_in_rebalanced != 0 else 0 + avg_out_expenditures_rate = (total_out_expenditures / total_out_rebalanced) * 1_000_000 if total_out_rebalanced != 0 else 0 + + rows.append([ + alias, + f"{in_forwarded:,}".replace(',', '_'), + f"{in_rate:,.0f}", + f"{in_earnings:,}".replace(',', '_'), + f"{out_forwarded:,}".replace(',', '_'), + f"{out_rate:,.0f}", + f"{out_earnings:,}".replace(',', '_'), + f"{in_rebalanced:,}".replace(',', '_'), + f"{in_rebalance_rate:,.0f}", + f"{in_expenditures:,}".replace(',', '_'), + f"{out_rebalanced:,}".replace(',', '_'), + f"{out_rebalance_rate:,.0f}", + f"{out_expenditures:,}".replace(',', '_'), + f"{net_earnings:,}".replace(',', '_'), + ]) + + # Divide the net earnings total by 2 + total_net_earnings /= 2 + + # Add a separator row + separator_row = ["-" * len(header) for header in [ + "Alias", + "In Forwarded", + "Rate", + "In Earn", + "Out Forwarded", + "Rate", + "Out Earn", + "In Rebal", + "Rate", + "In Exp", + "Out Rebal", + "Rate", + "Out Exp", + "Net Earn", + ]] + rows.append(separator_row) + + # Append the total row + rows.append([ + "TOTAL", + f"{total_in_forwarded:,}".replace(',', '_'), + f"{avg_in_earnings_rate:,.0f}", + f"{total_in_earnings:,}".replace(',', '_'), + f"{total_out_forwarded:,}".replace(',', '_'), + f"{avg_out_earnings_rate:,.0f}", + f"{total_out_earnings:,}".replace(',', '_'), + f"{total_in_rebalanced:,}".replace(',', '_'), + f"{avg_in_expenditures_rate:,.0f}", + f"{total_in_expenditures:,}".replace(',', '_'), + f"{total_out_rebalanced:,}".replace(',', '_'), + f"{avg_out_expenditures_rate:,.0f}", + f"{total_out_expenditures:,}".replace(',', '_'), + f"{int(total_net_earnings):,}".replace(',', '_'), + ]) + + return rows + +def main(): + parser = argparse.ArgumentParser(description="Run lightning-cli with specified network") + parser.add_argument('--mainnet', action='store_true', help='Run on mainnet') + parser.add_argument('--testnet', action='store_true', help='Run on testnet') + parser.add_argument('days', nargs='?', help='The number of days to pass to clboss-earnings-history (optional)') + + args = parser.parse_args() + + if args.testnet: + network_option = '--testnet' + else: + network_option = '--mainnet' # Default to mainnet if no option is specified + + if args.days: + earnings_data = run_lightning_cli_command(network_option, 'clboss-recent-earnings', str(args.days)) + else: + earnings_data = run_lightning_cli_command(network_option, 'clboss-recent-earnings') + + if earnings_data: + rows = calculate_net_earnings(earnings_data, network_option) + print(tabulate(rows, headers=[ + "Alias", + "In Forwarded", + "Rate", + "In Earn", + "Out Forwarded", + "Rate", + "Out Earn", + "In Rebal", + "Rate", + "In Exp", + "Out Rebal", + "Rate", + "Out Exp", + "Net Earn", + ],tablefmt="pretty", stralign="right", numalign="right")) + +if __name__ == "__main__": + main() diff --git a/contrib/clboss/__init__.py b/contrib/clboss/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/contrib/clboss/alias_cache.py b/contrib/clboss/alias_cache.py new file mode 100644 index 000000000..969bdc930 --- /dev/null +++ b/contrib/clboss/alias_cache.py @@ -0,0 +1,42 @@ +import os +import json + +# Define the cache directory and file path +CACHE_DIR = os.path.join(os.path.expanduser("~"), ".clboss") +CACHE_FILE = os.path.join(CACHE_DIR, "alias_cache.json") + +def load_cache(): + if os.path.exists(CACHE_FILE): + with open(CACHE_FILE, 'r') as f: + return json.load(f) + return {} + +def save_cache(cache): + # Ensure the cache directory exists + if not os.path.exists(CACHE_DIR): + os.makedirs(CACHE_DIR) + + with open(CACHE_FILE, 'w') as f: + json.dump(cache, f) + +def lookup_alias(run_lightning_cli_command, network_option, peer_id): + # Load the cache + cache = load_cache() + + # Check if the alias is already cached + if peer_id in cache: + return cache[peer_id] + + # Perform the lookup + alias = peer_id # Default to peer_id if alias not found + listnodes_data = run_lightning_cli_command(network_option, 'listnodes', peer_id) + if listnodes_data: + nodes = listnodes_data.get("nodes", []) + for node in nodes: + alias = node.get("alias", peer_id) # Fallback to peer_id if alias not found + + # Cache the result + cache[peer_id] = alias + save_cache(cache) + + return alias