1+ #!/usr/bin/env python3
2+
13import json
24import logging
35import time
46from pathlib import Path
5- from subprocess import run
6- from time import sleep
77from typing import Optional
88
99import click
1818 get_mission ,
1919 get_static_client ,
2020 wait_for_init ,
21- wait_for_pod ,
2221 write_file_to_container ,
2322)
2423from warnet .plugins import get_plugins_directory_or
2524from warnet .process import run_command
26- from warnet .status import _get_tank_status as network_status
2725
2826# To make a "mission" tag for your plugin, declare it using the variable name MISSION. This will
2927# be read by the warnet log system and status system.
@@ -54,8 +52,17 @@ class SimLNError(Exception):
5452# Each plugin must declare a click "group" by decorating a function named after the plugin.
5553# This makes your plugin available in the plugin section of Warnet.
5654@click .group ()
57- def simln ():
55+ @click .pass_context
56+ def simln (ctx ):
5857 """Commands for the SimLN plugin"""
58+ try :
59+ # check if we have set a user directory
60+ ctx .obj .get (USER_DIR_TAG )
61+ except Exception :
62+ # if not, set the great-grandparent of this file as the user dir
63+ ctx .ensure_object (dict )
64+ user_dir = Path (__file__ ).resolve ().parent .parent .parent
65+ ctx .obj [USER_DIR_TAG ] = Path (user_dir )
5966 pass
6067
6168
@@ -66,22 +73,6 @@ def warnet_register_plugin(register_command):
6673
6774# The group function name is then used in decorators to create commands. These commands are
6875# available to users when they access your plugin from the command line in Warnet.
69- @simln .command ()
70- def run_demo ():
71- """Run the SimLN Plugin demo"""
72- _init_network ()
73- _fund_wallets ()
74- _wait_for_all_ln_nodes_to_have_a_host ()
75- log .info (warnet ("bitcoin rpc tank-0000 -generate 7" ))
76- manual_open_channels ()
77- log .info (warnet ("bitcoin rpc tank-0000 -generate 7" ))
78- wait_for_gossip_sync (2 )
79- log .info ("done waiting" )
80- pod_name = prepare_and_launch_activity ()
81- log .info (pod_name )
82- wait_for_pod (pod_name , 60 )
83-
84-
8576@simln .command ()
8677def list_simln_podnames ():
8778 """Get a list of simln pod names"""
@@ -96,14 +87,6 @@ def download_results(pod_name: str):
9687 print (f"Downloaded results to: { dest } " )
9788
9889
99- def prepare_and_launch_activity () -> str :
100- sample_activity = _get_example_activity ()
101- log .info (f"Activity: { sample_activity } " )
102- pod_name = _launch_activity (sample_activity )
103- log .info ("Sent command. Done." )
104- return pod_name
105-
106-
10790# When we want to use a command inside our plugin and also provide that command to the user, we like
10891# to create a private function whose name starts with an underscore. We also make a public function
10992# with the same name except that we leave off the underscore, decorate it with the command
@@ -168,141 +151,6 @@ def launch_activity(ctx, activity: str):
168151 print (_launch_activity (parsed_activity , user_dir ))
169152
170153
171- def _init_network ():
172- """Mine regtest coins and wait for ln nodes to come online."""
173- log .info ("Initializing network" )
174- wait_for_all_tanks_status (target = "running" )
175-
176- warnet ("bitcoin rpc tank-0000 createwallet miner" )
177- warnet ("bitcoin rpc tank-0000 -generate 110" )
178- wait_for_predicate (lambda : int (warnet ("bitcoin rpc tank-0000 getblockcount" )) > 100 )
179-
180- def wait_for_all_ln_rpc ():
181- lns = get_mission (LIGHTNING_MISSION )
182- for v1_pod in lns :
183- ln = v1_pod .metadata .name
184- try :
185- warnet (f"ln rpc { ln } getinfo" )
186- except Exception :
187- log .info (f"LN node { ln } not ready for rpc yet" )
188- return False
189- return True
190-
191- wait_for_predicate (wait_for_all_ln_rpc )
192-
193-
194- @simln .command ()
195- def init_network ():
196- """Initialize the demo network."""
197- _init_network ()
198-
199-
200- def _fund_wallets ():
201- """Fund each ln node with 10 regtest coins."""
202- log .info ("Funding wallets" )
203- outputs = ""
204- lns = get_mission (LIGHTNING_MISSION )
205- for v1_pod in lns :
206- lnd = v1_pod .metadata .name
207- addr = json .loads (warnet (f"ln rpc { lnd } newaddress p2wkh" ))["address" ]
208- outputs += f',"{ addr } ":10'
209- # trim first comma
210- outputs = outputs [1 :]
211- log .info (warnet ("bitcoin rpc tank-0000 sendmany '' '{" + outputs + "}'" ))
212- log .info (warnet ("bitcoin rpc tank-0000 -generate 1" ))
213-
214-
215- @simln .command ()
216- def fund_wallets ():
217- """Fund each ln node with 10 regtest coins."""
218- _fund_wallets ()
219-
220-
221- def all_ln_nodes_have_a_host () -> bool :
222- """Find out if each ln node has a host."""
223- pods = get_mission (LIGHTNING_MISSION )
224- host_havers = 0
225- for pod in pods :
226- name = pod .metadata .name
227- result = warnet (f"ln host { name } " )
228- if len (result ) > 1 :
229- host_havers += 1
230- return host_havers == len (pods ) and host_havers != 0
231-
232-
233- @simln .command ()
234- def wait_for_all_ln_nodes_to_have_a_host ():
235- log .info (_wait_for_all_ln_nodes_to_have_a_host ())
236-
237-
238- def _wait_for_all_ln_nodes_to_have_a_host ():
239- wait_for_predicate (all_ln_nodes_have_a_host , timeout = 10 * 60 )
240-
241-
242- def wait_for_predicate (predicate , timeout = 5 * 60 , interval = 5 ):
243- log .info (
244- f"Waiting for predicate ({ predicate .__name__ } ) with timeout { timeout } s and interval { interval } s"
245- )
246- while timeout > 0 :
247- try :
248- if predicate ():
249- return
250- except Exception :
251- pass
252- sleep (interval )
253- timeout -= interval
254- import inspect
255-
256- raise Exception (
257- f"Timed out waiting for Truth from predicate: { inspect .getsource (predicate ).strip ()} "
258- )
259-
260-
261- def wait_for_all_tanks_status (target : str = "running" , timeout : int = 20 * 60 , interval : int = 5 ):
262- """Poll the warnet server for container status. Block until all tanks are running"""
263-
264- def check_status ():
265- tanks = network_status ()
266- stats = {"total" : 0 }
267- # "Probably" means all tanks are stopped and deleted
268- if len (tanks ) == 0 :
269- return True
270- for tank in tanks :
271- status = tank ["status" ]
272- stats ["total" ] += 1
273- stats [status ] = stats .get (status , 0 ) + 1
274- log .info (f"Waiting for all tanks to reach '{ target } ': { stats } " )
275- return target in stats and stats [target ] == stats ["total" ]
276-
277- wait_for_predicate (check_status , timeout , interval )
278-
279-
280- def wait_for_gossip_sync (expected : int = 2 ):
281- """Wait for any of the ln nodes to have an `expected` number of edges."""
282- log .info (f"Waiting for sync (expecting { expected } )..." )
283- current = 0
284- while current < expected :
285- current = 0
286- pods = get_mission (LIGHTNING_MISSION )
287- for v1_pod in pods :
288- node = v1_pod .metadata .name
289- chs = json .loads (run_command (f"warnet ln rpc { node } describegraph" ))["edges" ]
290- log .info (f"{ node } : { len (chs )} channels" )
291- current += len (chs )
292- sleep (1 )
293- log .info ("Synced" )
294-
295-
296- def warnet (cmd : str = "--help" ):
297- """Pass a `cmd` to Warnet."""
298- log .info (f"Executing warnet command: { cmd } " )
299- command = ["warnet" ] + cmd .split ()
300- proc = run (command , capture_output = True )
301- if proc .stderr :
302- raise Exception (proc .stderr .decode ().strip ())
303- return proc .stdout .decode ()
304-
305-
306154def _generate_activity_json (activity : list [dict ]) -> str :
307155 nodes = []
308156
@@ -321,46 +169,6 @@ def _generate_activity_json(activity: list[dict]) -> str:
321169 return json .dumps (data , indent = 2 )
322170
323171
324- def manual_open_channels ():
325- """Manually open channels between ln nodes 1, 2, and 3"""
326-
327- def wait_for_two_txs ():
328- wait_for_predicate (
329- lambda : json .loads (warnet ("bitcoin rpc tank-0000 getmempoolinfo" ))["size" ] == 2
330- )
331-
332- # 0 -> 1 -> 2
333- pk1 = warnet ("ln pubkey tank-0001-ln" )
334- pk2 = warnet ("ln pubkey tank-0002-ln" )
335- log .info (f"pk1: { pk1 } " )
336- log .info (f"pk2: { pk2 } " )
337-
338- host1 = ""
339- host2 = ""
340-
341- while not host1 or not host2 :
342- if not host1 :
343- host1 = warnet ("ln host tank-0001-ln" )
344- if not host2 :
345- host2 = warnet ("ln host tank-0002-ln" )
346- sleep (1 )
347-
348- print (
349- warnet (
350- f"ln rpc tank-0000-ln openchannel --node_key { pk1 } --local_amt 100000 --connect { host1 } "
351- )
352- )
353- print (
354- warnet (
355- f"ln rpc tank-0001-ln openchannel --node_key { pk2 } --local_amt 100000 --connect { host2 } "
356- )
357- )
358-
359- wait_for_two_txs ()
360-
361- warnet ("bitcoin rpc tank-0000 -generate 10" )
362-
363-
364172def _sh (pod , method : str , params : tuple [str , ...]) -> str :
365173 namespace = get_default_namespace ()
366174
@@ -403,5 +211,9 @@ def _sh(pod, method: str, params: tuple[str, ...]) -> str:
403211@click .argument ("method" , type = str )
404212@click .argument ("params" , type = str , nargs = - 1 ) # this will capture all remaining arguments
405213def sh (pod : str , method : str , params : tuple [str , ...]):
406- """Run commands on a pod"""
214+ """Run shell commands in a pod"""
407215 print (_sh (pod , method , params ))
216+
217+
218+ if __name__ == "__main__" :
219+ simln ()
0 commit comments