├── README.md ├── simpleFundsOverview ├── LICENSE ├── README.md └── funds.py └── rebalance-jit-routing ├── README.md └── rebalance.py /README.md: -------------------------------------------------------------------------------- 1 | # C-lightning-plugin-collection 2 | 3 | The c-lightning plugin collection is an open source project and archive for c-lightning plugins. 4 | 5 | If you have a usefull plugin feel free to make a pull request. 6 | Each plugin will be within a single folder and come with its own README and LICENSE file. 7 | 8 | This plugin collection is created and maintained by Rene Pickhardt. Check out his youtube channel 9 | at https://www.youtube.com/user/RenePickhardt if you want to learn more about lightning development. 10 | In Particular the plugin video at: https://youtu.be/FYs1I-pCJIg 11 | 12 | If you like my work feel free to support me on patreon: 13 | https://www.patreon.com/renepickhardt 14 | 15 | Or support the crowdfunding campaign of my book project about the lightning network at: 16 | https://tallyco.in/s/lnbook/ 17 | 18 | or leave me a tip on my donation page (comming from the donation plugin): 19 | https://ln.rene-pickhardt.de/ 20 | 21 | The work was partially sponsored by http://fulmo.org/ 22 | -------------------------------------------------------------------------------- /simpleFundsOverview/LICENSE: -------------------------------------------------------------------------------- 1 | Note: The code of this plugin is covered by the following (BSD-MIT) license: 2 | 3 | Copyright Rene Pickhardt 2018-2019. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /simpleFundsOverview/README.md: -------------------------------------------------------------------------------- 1 | # Funds overview plugin 2 | 3 | This plugin extends the c-lightning command line API with the `funds` command. 4 | Users can get a quick overview of their total funds, the offchain funds in 5 | channels and the onchain funds in unspent transaction outputs. 6 | 7 | run the plugin with: 8 | 9 | ``` 10 | lightningd --plugin=/path/to/c-lightning-plugin-collection/simpleFundsOverview/funds.py 11 | ``` 12 | 13 | Once the plugin is active you can run `lightning-cli help funds` 14 | to learn about the command line API. 15 | 16 | The easiest call will be `lightning-cli funds` without any additional arguments. 17 | 18 | you can create the plugin yourself by extending `fundslapp.py` following this 19 | video: https://youtu.be/FYs1I-pCJIg 20 | 21 | ## About the plugin 22 | This plugin was created and is maintained by Rene Pickhardt. It shall serve as 23 | an educational resource on his Youtube channel at: 24 | 25 | https://www.youtube.com/user/RenePickhardt 26 | 27 | The plugin is licensed like the rest of c-lightning with BSD-MIT license 28 | and comes without any warrenty (see LICENSE file) 29 | 30 | If you like my work feel free to support me on patreon: 31 | https://www.patreon.com/renepickhardt 32 | 33 | Or support the crowdfunding campaign of my book project about the lightning network at: 34 | https://tallyco.in/s/lnbook/ 35 | 36 | or leave me a tip on my donation page (comming from the donation plugin): 37 | https://ln.rene-pickhardt.de/ 38 | 39 | The work was partially sponsored by http://fulmo.org/ 40 | -------------------------------------------------------------------------------- /rebalance-jit-routing/README.md: -------------------------------------------------------------------------------- 1 | # Rebalance Plugin for JIT Routing 2 | 3 | This script is WORK IN PROGRESS and not fully running yet. 4 | 5 | The goal is to have a simple channel rebalancing plugin for c-lightning which can also be extended for JIT Routing (c.f.: https://lists.linuxfoundation.org/pipermail/lightning-dev/2019-March/001891.html ) 6 | 7 | The code has not been wrapped to the c-lightning plugin api. also there are currently dependancies to local files (which can be extracted from `lightning-cli` api calls. 8 | 9 | Overall as mentioned this script is work in progress. since I will not be working on it for the next 3 months I decided to publish the unfinnished script in case anyone wants to build on top of it. 10 | 11 | once finnished run the plugin with: 12 | 13 | ``` 14 | lightningd --plugin=/path/to/c-lightning-plugin-collection/simpleFundsOverview/rebalance.py 15 | ``` 16 | 17 | ## About the plugin 18 | This plugin was created and is maintained by Rene Pickhardt. It shall serve as 19 | an educational resource on his Youtube channel at: 20 | 21 | https://www.youtube.com/user/RenePickhardt 22 | 23 | The plugin is licensed like the rest of c-lightning with BSD-MIT license 24 | and comes without any warrenty (see LICENSE file) 25 | 26 | If you like my work feel free to support me on patreon: 27 | https://www.patreon.com/renepickhardt 28 | 29 | Or support the crowdfunding campaign of my book project about the lightning network at: 30 | https://tallyco.in/s/lnbook/ 31 | 32 | or leave me a tip on my donation page (comming from the donation plugin): 33 | https://ln.rene-pickhardt.de/ 34 | 35 | The work was partially sponsored by http://fulmo.org/ 36 | -------------------------------------------------------------------------------- /simpleFundsOverview/funds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ This plugin gives you a nicer overview of the funds that you own. 3 | 4 | Instead of calling listfunds and adding all outputs and channels 5 | this plugin does that for you. 6 | 7 | Activate the plugin with: 8 | `lightningd --plugin=PATH/TO/LIGHTNING/contrib/plugins/funds/funds.py` 9 | 10 | Call the plugin with: 11 | `lightning-cli funds` 12 | 13 | The standard unit to depict the funds is set to satoshis. 14 | The unit can be changed by and arguments after `lightning-cli funds` 15 | for each call. It is also possible to change the standard unit when 16 | starting lightningd just pass `--funds_display_unit={unit}` where 17 | unit can be s for satoshi, b for bits, m for milliBitcoin and B for BTC. 18 | 19 | 20 | Author: Rene Pickhardt (https://ln.rene-pickhardt.de) 21 | Development of the plugin was sponsored by fulmo.org 22 | You can also support future work at https://tallyco.in/s/lnbook/ 23 | """ 24 | 25 | import json 26 | 27 | from lightning.lightning import LightningRpc 28 | from lightning.plugin import Plugin 29 | from os.path import join 30 | 31 | rpc_interface = None 32 | plugin = Plugin(autopatch=True) 33 | 34 | unit_aliases = { 35 | "bitcoin": "BTC", 36 | "btc": "BTC", 37 | "satoshi": "sat", 38 | "satoshis": "sat", 39 | "bit": "bit", 40 | "bits": "bit", 41 | "milli": "mBTC", 42 | "mbtc": "mBTC", 43 | "millibtc": "mBTC", 44 | "B": "BTC", 45 | "s": "sat", 46 | "m": "mBTC", 47 | "b": "bit", 48 | } 49 | 50 | unit_divisor = { 51 | "sat": 1, 52 | "bit": 100, 53 | "mBTC": 100*1000, 54 | "BTC": 100*1000*1000, 55 | } 56 | 57 | 58 | @plugin.method("funds") 59 | def funds(unit=None, plugin=None): 60 | """Lists the total funds the lightning node owns off- and onchain in {unit}. 61 | 62 | {unit} can take the following values: 63 | s, satoshi, satoshis to depict satoshis 64 | b, bit, bits to depict bits 65 | m, milli, btc to depict milliBitcoin 66 | B, bitcoin, btc to depict Bitcoins 67 | 68 | When not using Satoshis (default) the comma values are rounded off.""" 69 | 70 | plugin.log("call with unit: {}".format(unit), level="debug") 71 | if unit is None: 72 | unit = plugin.get_option("funds_display_unit") 73 | 74 | if unit != "B": 75 | unit = unit_aliases.get(unit.lower(), "sat") 76 | else: 77 | unit = "BTC" 78 | 79 | div = unit_divisor.get(unit, 1) 80 | 81 | funds = rpc_interface.listfunds() 82 | 83 | onchain_value = sum([int(x["value"]) for x in funds["outputs"]]) 84 | offchain_value = sum([int(x["channel_sat"]) for x in funds["channels"]]) 85 | 86 | total_funds = onchain_value + offchain_value 87 | 88 | return { 89 | 'total_'+unit: total_funds//div, 90 | 'onchain_'+unit: onchain_value//div, 91 | 'offchain_'+unit: offchain_value//div, 92 | } 93 | 94 | 95 | @plugin.method("init") 96 | def init(options, configuration, plugin): 97 | global rpc_interface 98 | plugin.log("start initialization of the funds plugin", level="debug") 99 | basedir = configuration['lightning-dir'] 100 | rpc_filename = configuration['rpc-file'] 101 | path = join(basedir, rpc_filename) 102 | plugin.log("rpc interface located at {}".format(path)) 103 | rpc_interface = LightningRpc(path) 104 | plugin.log("Funds Plugin successfully initialezed") 105 | plugin.log("standard unit is set to {}".format( 106 | plugin.get_option("funds_display_unit")), level="debug") 107 | 108 | 109 | # set the standard display unit to satoshis 110 | plugin.add_option('funds_display_unit', 's', 111 | 'pass the unit which should be used by default for the simple funds overview plugin') 112 | plugin.run() 113 | -------------------------------------------------------------------------------- /rebalance-jit-routing/rebalance.py: -------------------------------------------------------------------------------- 1 | from lightning import LightningRpc 2 | from operator import itemgetter 3 | 4 | import json 5 | import networkx as nx 6 | 7 | 8 | class Network: 9 | """ retrieves the lightning network and provides a pruned view of the extended ego network 10 | 11 | """ 12 | 13 | def __compute_friends(self): 14 | self.__friends = set(channel["peer_id"] 15 | for channel in self.__own_channels) 16 | print(len(self.__friends)) 17 | 18 | def __compute_pruned_extended_egonetwork(self): 19 | """ Computes the friend of a friend network without own channels 20 | 21 | """ 22 | foaf_network = [] 23 | for u, v in self.__lightning_network.edges(): 24 | channel = self.__lightning_network[u][v] 25 | if channel["source"] in self.__friends or channel["destination"] in self.__friends: 26 | foaf_network.append(self.__lightning_network[u][v]) 27 | 28 | peer_counter = {} 29 | for channel in foaf_network: 30 | src = channel["source"] 31 | if src not in peer_counter: 32 | peer_counter[src] = 1 33 | else: 34 | peer_counter[src] += 1 35 | 36 | to_remove = set( 37 | key for key, value in peer_counter.items() if value == 1) 38 | 39 | # node_ids = set(k for k, v in peer_counter.items() if v > 1) 40 | # print(len(node_ids)) 41 | 42 | final_ego_network = [] 43 | for channel in foaf_network: 44 | src = channel["source"] 45 | dest = channel["destination"] 46 | if src == self.__own_node_id or dest == self.__own_node_id: 47 | continue 48 | if src not in to_remove and dest not in to_remove: 49 | final_ego_network.append(channel) 50 | 51 | self.__pruned_ln = nx.DiGraph() 52 | for channel in final_ego_network: 53 | self.__pruned_ln.add_edge( 54 | channel["source"], channel["destination"], **channel) 55 | 56 | # print(len(foaf_network)) 57 | # print(len(final_ego_network)) 58 | 59 | def __init__(self, network, own_channels, node_id): 60 | print("initialized the Network maintainer") 61 | self.__own_node_id = node_id 62 | self.__lightning_network = nx.DiGraph() 63 | for channel in network: 64 | self.__lightning_network.add_edge( 65 | channel["source"], channel["destination"], **channel) 66 | self.__own_channels = own_channels 67 | self.__compute_friends() 68 | self.__compute_pruned_extended_egonetwork() 69 | 70 | def get_pruned_network(self): 71 | return self.__pruned_ln 72 | 73 | def get_full_network(self): 74 | return self.__lightning_network 75 | 76 | 77 | class EgoNetwork(): 78 | 79 | def __init__(self, own_channels): 80 | self.__channels = {} 81 | for channel in own_channels: 82 | self.__channels[channel["peer_id"]] = channel 83 | 84 | def liquidity_stats(self, node_id): 85 | channel = self.__channels[node_id] 86 | ours = int(channel["channel_sat"]) 87 | cap = int(channel["channel_total_sat"]) 88 | rel = float(ours)/float(cap) 89 | return "us:{:10} total:{:10} relative: {:4.2f}".format(ours, cap, rel) 90 | 91 | 92 | class ChannelSuggester(): 93 | """ checks the balances of channels and suggest to rebalance 94 | 95 | FIXME: could go a way from relative boundaries and always work on top / flop channels 96 | """ 97 | 98 | def __get_sorted_channels(self): 99 | channel_list = [] 100 | for channel in self.__local_channels: 101 | channel_sat = int(channel["channel_sat"]) 102 | channel_total_sat = int(channel["channel_total_sat"]) 103 | channel_list.append( 104 | (float(channel_sat)/float(channel_total_sat), channel)) 105 | channel_list.sort(key=itemgetter(0)) 106 | return channel_list 107 | 108 | def __init__(self, local_channels_with_balance, min_incoming_capacity=0.01, max_outgoing_capacity=0.99): 109 | print("initialized the channel suggester") 110 | self.__local_channels = local_channels_with_balance 111 | self.__min_incoming_capacity = min_incoming_capacity 112 | self.__max_outgoing_capacity = max_outgoing_capacity 113 | 114 | def is_need_to_balance(self): 115 | channels = self.__get_sorted_channels() 116 | if len(channels) < 2: 117 | print("not enough channels to do a balancing operation") 118 | return False 119 | if channels[0][0] > self.__min_incoming_capacity: 120 | print("not enough incomming capacity to rebalance") 121 | return False 122 | if channels[-1][0] < self.__max_outgoing_capacity: 123 | print("not enough outgoing capacity to rebalance") 124 | return False 125 | return True 126 | 127 | def get_dry_channels(self): 128 | channels = [chan for chan in self.__get_sorted_channels( 129 | ) if chan[0] < self.__min_incoming_capacity] 130 | return channels 131 | 132 | def get_liquid_channels(self): 133 | channels = [chan for chan in self.__get_sorted_channels( 134 | ) if chan[0] > self.__max_outgoing_capacity] 135 | return channels 136 | 137 | 138 | class PeerAnalyzer(): 139 | 140 | def __list_channel_ratios(self, offered_str, fulfilled_str): 141 | for peer in self.__peers: 142 | for channel in peer["channels"]: 143 | offered = int(channel[offered_str]) 144 | if offered > 0: 145 | fulfilled = int(channel[fulfilled_str]) 146 | print("{:4.2f}\t{}\t{}".format(float(fulfilled)/offered, 147 | offered, fulfilled), channel["channel_id"], channel["short_channel_id"]) 148 | 149 | def __list_in_ratios(self): 150 | print("inratios") 151 | self.__list_channel_ratios( 152 | "in_payments_offered", "in_payments_fulfilled") 153 | print("") 154 | 155 | def __list_out_ratios(self): 156 | print("outratios") 157 | self.__list_channel_ratios( 158 | "out_payments_offered", "out_payments_fulfilled") 159 | print() 160 | 161 | def __init__(self): 162 | f = open("/Users/rpickhardt/hacken/plugindev/peers20190310.json", "r") 163 | jsn = json.load(f) 164 | self.__peers = jsn["peers"] 165 | self.__list_in_ratios() 166 | self.__list_out_ratios() 167 | 168 | 169 | class CycleSuggester(): 170 | def __init__(self, network): 171 | self.__network = network 172 | 173 | def paths(self, start, end): 174 | return list(nx.all_simple_paths(self.__network, start, end, 3)) 175 | 176 | 177 | class FeeCalculator(): 178 | 179 | def __node_id_path_to_channels(self, path): 180 | channels = [] 181 | for i in range(len(path)-1): 182 | src = path[i] 183 | dest = path[i+1] 184 | channel = self.__network[src][dest] 185 | channels.append(channel) 186 | # print(channel) 187 | return channels 188 | 189 | def __onion_from_channels(self, amount, channels): 190 | route = [] 191 | item = {} 192 | item["msatoshi"] = amount 193 | item["channel"] = channels[-1]["short_channel_id"] 194 | # FIXME: how can we know this for abitrary cases? 195 | item["delay"] = 10 196 | item["id"] = channels[-1]["destination"] 197 | route.append(item) 198 | for i in range(len(channels)-1, 0, -1): 199 | old = route[-1] 200 | item = {} 201 | item["msatoshi"] = old["msatoshi"] + \ 202 | int(channels[i]["base_fee_millisatoshi"]) + old["msatoshi"] * \ 203 | int(channels[i]["fee_per_millionth"]) // 1000000 204 | item["channel"] = channels[i-1]["short_channel_id"] 205 | item["delay"] = old["delay"] + int(channels[i]["delay"]) 206 | item["id"] = channels[i]["source"] 207 | route.append(item) 208 | return list(reversed(route)) 209 | 210 | def __init__(self, network): 211 | self.__network = network 212 | 213 | def compute_fee_for_path(self, amount, path): 214 | channels = self.__node_id_path_to_channels(path) 215 | onion_route = self.__onion_from_channels(amount, channels) 216 | return onion_route[0]["msatoshi"] - amount 217 | # print(onion_route) 218 | 219 | 220 | if __name__ == "__main__": 221 | pa = PeerAnalyzer() 222 | exit() 223 | ln = LightningRpc("/home/rpickhardt/.lightning/lightning-rpc") 224 | own_channels = None 225 | try: 226 | f = open( 227 | "/Users/rpickhardt/hacken/lightning-helpers/balance-channels/friends20190301.json") 228 | own_channels = json.load(f)["channels"] 229 | except: 230 | own_channels = ln.listfunds()["channels"] 231 | 232 | ego_network = EgoNetwork(own_channels) 233 | 234 | channel_suggester = ChannelSuggester(own_channels, 0.25, 0.5) 235 | if channel_suggester.is_need_to_balance(): 236 | print("channel balancing is suggested") 237 | # print("channels with too little outgoing capacity:") 238 | # for chan in channel_suggester.get_dry_channels(): 239 | # print(chan) 240 | print("channels with too little incoming capacity:") 241 | for chan in channel_suggester.get_liquid_channels(): 242 | print(chan) 243 | 244 | channels = None 245 | try: 246 | f = open( 247 | "/Users/rpickhardt/hacken/lightning-helpers/balance-channels/channels20190301.json") 248 | channels = json.load(f)["channels"] 249 | except: 250 | channels = ln.listchannels()["channels"] 251 | 252 | own_node_id = "03efccf2c383d7bf340da9a3f02e2c23104a0e4fe8ac1a880c8e2dc92fbdacd9df" 253 | network = Network(channels, own_channels, own_node_id) 254 | print(len(channels)) 255 | 256 | sug = CycleSuggester(network.get_pruned_network()) 257 | # sug = CycleSuggester(network.get_full_network()) 258 | 259 | flag = False 260 | fee_calculator = FeeCalculator(network.get_full_network()) 261 | for source in channel_suggester.get_liquid_channels(): 262 | for dest in channel_suggester.get_dry_channels(): 263 | try: 264 | src = source[1]["peer_id"] 265 | dest = dest[1]["peer_id"] 266 | # print(src, dest) 267 | paths = sug.paths(src, dest) 268 | if len(paths) == 0: 269 | continue 270 | minfee = 10000000 271 | bpath = None 272 | for p in paths: 273 | path = [own_node_id] 274 | path.extend(p) 275 | path.append(own_node_id) 276 | fee = fee_calculator.compute_fee_for_path(100000000, path) 277 | if fee < minfee: 278 | minfee = fee 279 | bpath = path 280 | print(minfee, bpath) 281 | # sat = network.get_full_network() 282 | # print(sat[src][dest]) 283 | # sat = sat[src][dest]["channel_sat"] 284 | # cap = network.get_pruned_network( 285 | # )[src][dest]["channel_total_sat"] 286 | f_stats = ego_network.liquidity_stats(src) 287 | t_stats = ego_network.liquidity_stats(dest) 288 | print("found {:6} paths from {} with {} to {} with {} ".format( 289 | len(paths), src, f_stats, dest, t_stats)) 290 | # flag = True 291 | except Exception as e: 292 | # print(e) 293 | # print(source, dest) 294 | pass 295 | if flag: 296 | break 297 | print() 298 | if flag: 299 | break 300 | 301 | path = [own_node_id, "03c4bb19c3a388d790968328b0f0d187a1a28597d3ad082200a47baadfdb6aee8d", 302 | "020e56a13babec99abdc2c4afbe34e1e44230d79b234c059fd4ff1e367765fdb1b", 303 | "02e2670a2c2661a9eea13b7cfdcdd7f552f591b9ee60e5678b7abe77b7f9516f96", 304 | "03ee180e8ee07f1f9c9987d98b5d5decf6bad7d058bdd8be3ad97c8e0dd2cdc7ba"] 305 | print(fee_calculator.compute_fee_for_path(1000000, path)) 306 | # print(sug.paths(channel_suggester.get_liquid_channels() 307 | # [-1], channel_suggester.get_dry_channels()[0])) 308 | --------------------------------------------------------------------------------