1+ import json
12from io import BytesIO
23from pathlib import Path
34
@@ -28,7 +29,79 @@ def create(number: int, outfile: Path, version: str, bitcoin_conf: Path, random:
2829 if outfile :
2930 file_path = Path (outfile )
3031 nx .write_graphml (graph , file_path , named_key_ids = True )
31- return f"Generated graph written to file: { outfile } "
32+ bio = BytesIO ()
33+ nx .write_graphml (graph , bio , named_key_ids = True )
34+ xml_data = bio .getvalue ()
35+ print (xml_data .decode ("utf-8" ))
36+
37+
38+ @graph .command ()
39+ @click .argument ("infile" , type = click .Path ())
40+ @click .option ("--outfile" , type = click .Path ())
41+ def import_json (infile : Path , outfile : Path ):
42+ """
43+ Create a cycle graph with nodes imported from lnd `describegraph` JSON file,
44+ and additionally include 7 extra random outbounds per node. Include lightning
45+ channels and their policies as well.
46+ Returns XML file as string with or without --outfile option.
47+ """
48+ with open (infile ) as f :
49+ json_graph = json .loads (f .read ())
50+
51+ # Start with a connected L1 graph with the right amount of tanks
52+ graph = create_cycle_graph (len (json_graph ["nodes" ]), version = DEFAULT_TAG , bitcoin_conf = None , random_version = False )
53+
54+ # Initialize all the tanks with basic LN node configurations
55+ for index , n in enumerate (graph .nodes ()):
56+ graph .nodes [n ]["bitcoin_config" ] = f"-uacomment=tank{ index :06} "
57+ graph .nodes [n ]["ln" ] = "lnd"
58+ graph .nodes [n ]["ln_cb_image" ] = "pinheadmz/circuitbreaker:278737d"
59+ graph .nodes [n ]["ln_config" ] = "--protocol.wumbo-channels"
60+
61+ # Save a map of LN pubkey -> Tank index
62+ ln_ids = {}
63+ for index , node in enumerate (json_graph ["nodes" ]):
64+ ln_ids [node ["id" ]] = index
65+
66+ # Offset for edge IDs
67+ # Note create_cycle_graph() creates L1 edges all with the same id "0"
68+ L1_edges = len (graph .edges )
69+
70+ # Insert LN channels
71+ # Ensure channels are in order by channel ID like lnd describegraph output
72+ sorted_edges = sorted (json_graph ["edges" ], key = lambda chan : int (chan ['channel_id' ]))
73+ for ln_index , channel in enumerate (sorted_edges ):
74+ src = ln_ids [channel ["node1_pub" ]]
75+ tgt = ln_ids [channel ["node2_pub" ]]
76+ cap = int (channel ["capacity" ])
77+ push = cap // 2
78+ openp = f"--local_amt={ cap } --push_amt={ push } "
79+ srcp = ""
80+ tgtp = ""
81+ if channel ["node1_policy" ]:
82+ srcp += f" --base_fee_msat={ channel ['node1_policy' ]['fee_base_msat' ]} "
83+ srcp += f" --fee_rate_ppm={ channel ['node1_policy' ]['fee_rate_milli_msat' ]} "
84+ srcp += f" --time_lock_delta={ channel ['node1_policy' ]['time_lock_delta' ]} "
85+ srcp += f" --min_htlc_msat={ channel ['node1_policy' ]['min_htlc' ]} "
86+ srcp += f" --max_htlc_msat={ push * 1000 } "
87+ if channel ["node2_policy" ]:
88+ tgtp += f" --base_fee_msat={ channel ['node2_policy' ]['fee_base_msat' ]} "
89+ tgtp += f" --fee_rate_ppm={ channel ['node2_policy' ]['fee_rate_milli_msat' ]} "
90+ tgtp += f" --time_lock_delta={ channel ['node2_policy' ]['time_lock_delta' ]} "
91+ tgtp += f" --min_htlc_msat={ channel ['node2_policy' ]['min_htlc' ]} "
92+ tgtp += f" --max_htlc_msat={ push * 1000 } "
93+
94+ graph .add_edge (
95+ src ,
96+ tgt ,
97+ key = ln_index + L1_edges ,
98+ channel_open = openp ,
99+ source_policy = srcp ,
100+ target_policy = tgtp )
101+
102+ if outfile :
103+ file_path = Path (outfile )
104+ nx .write_graphml (graph , file_path , named_key_ids = True )
32105 bio = BytesIO ()
33106 nx .write_graphml (graph , bio , named_key_ids = True )
34107 xml_data = bio .getvalue ()
@@ -42,5 +115,5 @@ def validate(graph: Path):
42115 Validate a <graph file> against the schema.
43116 """
44117 with open (graph ) as f :
45- graph = nx .parse_graphml (f .read (), node_type = int )
118+ graph = nx .parse_graphml (f .read (), node_type = int , force_multigraph = True )
46119 return validate_graph_schema (graph )
0 commit comments