1
+ #!/usr/bin/env python3
2
+
1
3
import json
2
4
import logging
3
5
import time
4
6
from pathlib import Path
5
- from subprocess import run
6
- from time import sleep
7
7
from typing import Optional
8
8
9
9
import click
18
18
get_mission ,
19
19
get_static_client ,
20
20
wait_for_init ,
21
- wait_for_pod ,
22
21
write_file_to_container ,
23
22
)
24
23
from warnet .plugins import get_plugins_directory_or
25
24
from warnet .process import run_command
26
- from warnet .status import _get_tank_status as network_status
27
25
28
26
# To make a "mission" tag for your plugin, declare it using the variable name MISSION. This will
29
27
# be read by the warnet log system and status system.
@@ -54,8 +52,17 @@ class SimLNError(Exception):
54
52
# Each plugin must declare a click "group" by decorating a function named after the plugin.
55
53
# This makes your plugin available in the plugin section of Warnet.
56
54
@click .group ()
57
- def simln ():
55
+ @click .pass_context
56
+ def simln (ctx ):
58
57
"""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 )
59
66
pass
60
67
61
68
@@ -66,22 +73,6 @@ def warnet_register_plugin(register_command):
66
73
67
74
# The group function name is then used in decorators to create commands. These commands are
68
75
# 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
-
85
76
@simln .command ()
86
77
def list_simln_podnames ():
87
78
"""Get a list of simln pod names"""
@@ -96,14 +87,6 @@ def download_results(pod_name: str):
96
87
print (f"Downloaded results to: { dest } " )
97
88
98
89
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
-
107
90
# When we want to use a command inside our plugin and also provide that command to the user, we like
108
91
# to create a private function whose name starts with an underscore. We also make a public function
109
92
# 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):
168
151
print (_launch_activity (parsed_activity , user_dir ))
169
152
170
153
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
-
306
154
def _generate_activity_json (activity : list [dict ]) -> str :
307
155
nodes = []
308
156
@@ -321,46 +169,6 @@ def _generate_activity_json(activity: list[dict]) -> str:
321
169
return json .dumps (data , indent = 2 )
322
170
323
171
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
-
364
172
def _sh (pod , method : str , params : tuple [str , ...]) -> str :
365
173
namespace = get_default_namespace ()
366
174
@@ -403,5 +211,9 @@ def _sh(pod, method: str, params: tuple[str, ...]) -> str:
403
211
@click .argument ("method" , type = str )
404
212
@click .argument ("params" , type = str , nargs = - 1 ) # this will capture all remaining arguments
405
213
def sh (pod : str , method : str , params : tuple [str , ...]):
406
- """Run commands on a pod"""
214
+ """Run shell commands in a pod"""
407
215
print (_sh (pod , method , params ))
216
+
217
+
218
+ if __name__ == "__main__" :
219
+ simln ()
0 commit comments