neuronum 9.0.0__py3-none-any.whl → 10.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of neuronum might be problematic. Click here for more details.
- cli/main.py +229 -945
- neuronum/__init__.py +1 -1
- neuronum/neuronum.py +671 -290
- neuronum-10.0.1.dist-info/METADATA +159 -0
- neuronum-10.0.1.dist-info/RECORD +10 -0
- neuronum-9.0.0.dist-info/METADATA +0 -124
- neuronum-9.0.0.dist-info/RECORD +0 -10
- {neuronum-9.0.0.dist-info → neuronum-10.0.1.dist-info}/WHEEL +0 -0
- {neuronum-9.0.0.dist-info → neuronum-10.0.1.dist-info}/entry_points.txt +0 -0
- {neuronum-9.0.0.dist-info → neuronum-10.0.1.dist-info}/licenses/LICENSE.md +0 -0
- {neuronum-9.0.0.dist-info → neuronum-10.0.1.dist-info}/top_level.txt +0 -0
cli/main.py
CHANGED
|
@@ -1,314 +1,52 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
import os
|
|
3
|
-
import platform
|
|
4
|
-
import glob
|
|
5
|
-
import asyncio
|
|
6
|
-
import aiohttp
|
|
7
1
|
import click
|
|
8
2
|
import questionary
|
|
9
3
|
from pathlib import Path
|
|
10
4
|
import requests
|
|
11
|
-
import
|
|
12
|
-
from datetime import datetime
|
|
13
|
-
import sys
|
|
14
|
-
import json
|
|
5
|
+
from cryptography.hazmat.primitives import hashes
|
|
15
6
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
16
7
|
from cryptography.hazmat.primitives import serialization
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
click.echo("Visit https://neuronum.net/createcell to create your Neuronum Business Cell")
|
|
37
|
-
|
|
38
|
-
if cell_type == "community":
|
|
39
|
-
|
|
40
|
-
email = click.prompt("Enter email")
|
|
41
|
-
password = click.prompt("Enter password", hide_input=True)
|
|
42
|
-
repeat_password = click.prompt("Repeat password", hide_input=True)
|
|
43
|
-
|
|
44
|
-
if password != repeat_password:
|
|
45
|
-
click.echo("Passwords do not match!")
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
url = f"https://{network}/api/create_cell/{cell_type}"
|
|
49
|
-
|
|
50
|
-
create_cell = {"email": email, "password": password}
|
|
51
|
-
|
|
52
|
-
try:
|
|
53
|
-
response = requests.post(url, json=create_cell)
|
|
54
|
-
response.raise_for_status()
|
|
55
|
-
status = response.json()["status"]
|
|
56
|
-
|
|
57
|
-
except requests.exceptions.RequestException as e:
|
|
58
|
-
click.echo(f"Error sending request: {e}")
|
|
59
|
-
return
|
|
60
|
-
|
|
61
|
-
if status == True:
|
|
62
|
-
host = response.json()["host"]
|
|
63
|
-
cellkey = click.prompt(f"Please verify your email address with the Cell Key send to {email}")
|
|
64
|
-
|
|
65
|
-
url = f"https://{network}/api/verify_email"
|
|
66
|
-
|
|
67
|
-
verify_email = {"host": host, "email": email, "cellkey": cellkey}
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
response = requests.post(url, json=verify_email)
|
|
71
|
-
response.raise_for_status()
|
|
72
|
-
status = response.json()["status"]
|
|
73
|
-
|
|
74
|
-
except requests.exceptions.RequestException as e:
|
|
75
|
-
click.echo(f"Error sending request: {e}")
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
if status == True:
|
|
79
|
-
synapse = response.json()["synapse"]
|
|
80
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
81
|
-
credentials_folder_path.mkdir(parents=True, exist_ok=True)
|
|
82
|
-
|
|
83
|
-
env_path = credentials_folder_path / ".env"
|
|
84
|
-
env_path.write_text(f"HOST={host}\nPASSWORD={password}\nNETWORK={network}\nSYNAPSE={synapse}\n")
|
|
85
|
-
|
|
86
|
-
click.echo(f"Welcome to Neuronum! Community Cell '{host}' created and connected!")
|
|
87
|
-
|
|
88
|
-
if status == False:
|
|
89
|
-
click.echo(f"Error:'{email}' already assigned!")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@click.command()
|
|
93
|
-
def connect_cell():
|
|
94
|
-
email = click.prompt("Enter your Email")
|
|
95
|
-
password = click.prompt("Enter password", hide_input=True)
|
|
96
|
-
|
|
97
|
-
network = questionary.select(
|
|
98
|
-
"Choose Network:",
|
|
99
|
-
choices=["neuronum.net"]
|
|
100
|
-
).ask()
|
|
101
|
-
|
|
102
|
-
url = f"https://{network}/api/connect_cell"
|
|
103
|
-
payload = {"email": email, "password": password}
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
response = requests.post(url, json=payload)
|
|
107
|
-
response.raise_for_status()
|
|
108
|
-
status = response.json()["status"]
|
|
109
|
-
host = response.json()["host"]
|
|
110
|
-
except requests.exceptions.RequestException as e:
|
|
111
|
-
click.echo(f"Error connecting: {e}")
|
|
112
|
-
return
|
|
113
|
-
|
|
114
|
-
if status == True:
|
|
115
|
-
cellkey = click.prompt(f"Please verify your email address with the Cell Key send to {email}")
|
|
116
|
-
url = f"https://{network}/api/verify_email"
|
|
117
|
-
verify_email = {"host": host, "email": email, "cellkey": cellkey}
|
|
118
|
-
|
|
119
|
-
try:
|
|
120
|
-
response = requests.post(url, json=verify_email)
|
|
121
|
-
response.raise_for_status()
|
|
122
|
-
status = response.json()["status"]
|
|
123
|
-
synapse = response.json()["synapse"]
|
|
124
|
-
|
|
125
|
-
except requests.exceptions.RequestException as e:
|
|
126
|
-
click.echo(f"Error sending request: {e}")
|
|
127
|
-
return
|
|
128
|
-
|
|
129
|
-
if status == True:
|
|
130
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
131
|
-
credentials_folder_path.mkdir(parents=True, exist_ok=True)
|
|
132
|
-
|
|
133
|
-
env_path = credentials_folder_path / f".env"
|
|
134
|
-
env_path.write_text(f"HOST={host}\nPASSWORD={password}\nNETWORK={network}\nSYNAPSE={synapse}\n")
|
|
135
|
-
|
|
136
|
-
click.echo(f"Cell '{host}' connected!")
|
|
137
|
-
else:
|
|
138
|
-
click.echo(f"Connection failed!")
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
@click.command()
|
|
142
|
-
def view_cell():
|
|
143
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
144
|
-
env_path = credentials_folder_path / ".env"
|
|
145
|
-
|
|
146
|
-
env_data = {}
|
|
147
|
-
|
|
148
|
-
try:
|
|
149
|
-
with open(env_path, "r") as f:
|
|
150
|
-
for line in f:
|
|
151
|
-
key, value = line.strip().split("=")
|
|
152
|
-
env_data[key] = value
|
|
153
|
-
|
|
154
|
-
host = env_data.get("HOST", "")
|
|
155
|
-
|
|
156
|
-
except FileNotFoundError:
|
|
157
|
-
click.echo("Error: No credentials found. Please connect to a cell first.")
|
|
158
|
-
return
|
|
159
|
-
except Exception as e:
|
|
160
|
-
click.echo(f"Error reading .env file: {e}")
|
|
161
|
-
return
|
|
162
|
-
|
|
163
|
-
if host:
|
|
164
|
-
click.echo(f"Connected Cell: '{host}'")
|
|
165
|
-
else:
|
|
166
|
-
click.echo("No active cell connection found.")
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
@click.command()
|
|
170
|
-
def disconnect_cell():
|
|
171
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
172
|
-
env_path = credentials_folder_path / ".env"
|
|
173
|
-
|
|
174
|
-
env_data = {}
|
|
175
|
-
|
|
176
|
-
try:
|
|
177
|
-
with open(env_path, "r") as f:
|
|
178
|
-
for line in f:
|
|
179
|
-
key, value = line.strip().split("=")
|
|
180
|
-
env_data[key] = value
|
|
181
|
-
|
|
182
|
-
host = env_data.get("HOST", "")
|
|
183
|
-
|
|
184
|
-
except FileNotFoundError:
|
|
185
|
-
click.echo("Error: .env with credentials not found")
|
|
186
|
-
return
|
|
187
|
-
except Exception as e:
|
|
188
|
-
click.echo(f"Error reading .env file: {e}")
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
if env_path.exists():
|
|
192
|
-
if click.confirm(f"Are you sure you want to disconnect Cell '{host}'?", default=True):
|
|
193
|
-
os.remove(env_path)
|
|
194
|
-
click.echo(f"'{host}' disconnected!")
|
|
195
|
-
else:
|
|
196
|
-
click.echo("Disconnect canceled.")
|
|
197
|
-
else:
|
|
198
|
-
click.echo(f"No Neuronum Cell connected!")
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
@click.command()
|
|
202
|
-
def delete_cell():
|
|
203
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
204
|
-
env_path = credentials_folder_path / ".env"
|
|
205
|
-
|
|
206
|
-
env_data = {}
|
|
207
|
-
|
|
208
|
-
try:
|
|
209
|
-
with open(env_path, "r") as f:
|
|
210
|
-
for line in f:
|
|
211
|
-
key, value = line.strip().split("=")
|
|
212
|
-
env_data[key] = value
|
|
213
|
-
|
|
214
|
-
host = env_data.get("HOST", "")
|
|
215
|
-
password = env_data.get("PASSWORD", "")
|
|
216
|
-
network = env_data.get("NETWORK", "")
|
|
217
|
-
synapse = env_data.get("SYNAPSE", "")
|
|
218
|
-
|
|
219
|
-
except FileNotFoundError:
|
|
220
|
-
click.echo("Error: No cell connected. Connect Cell first to delete")
|
|
221
|
-
return
|
|
222
|
-
except Exception as e:
|
|
223
|
-
click.echo(f"Error reading .env file: {e}")
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
confirm = click.confirm(f" Are you sure you want to delete '{host}'?", default=True)
|
|
227
|
-
os.remove(env_path)
|
|
228
|
-
if not confirm:
|
|
229
|
-
click.echo("Deletion canceled.")
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
url = f"https://{network}/api/delete_cell"
|
|
233
|
-
payload = {"host": host, "password": password, "synapse": synapse}
|
|
234
|
-
|
|
8
|
+
from cryptography.hazmat.backends import default_backend
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey
|
|
10
|
+
import base64
|
|
11
|
+
import time
|
|
12
|
+
import hashlib
|
|
13
|
+
from bip_utils import Bip39MnemonicGenerator, Bip39SeedGenerator
|
|
14
|
+
from bip_utils import Bip39MnemonicValidator, Bip39Languages
|
|
15
|
+
|
|
16
|
+
# --- Configuration Constants ---
|
|
17
|
+
NEURONUM_PATH = Path.home() / ".neuronum"
|
|
18
|
+
ENV_FILE = NEURONUM_PATH / ".env"
|
|
19
|
+
PUBLIC_KEY_FILE = NEURONUM_PATH / "public_key.pem"
|
|
20
|
+
PRIVATE_KEY_FILE = NEURONUM_PATH / "private_key.pem"
|
|
21
|
+
API_BASE_URL = "https://neuronum.net/api"
|
|
22
|
+
|
|
23
|
+
# --- Utility Functions ---
|
|
24
|
+
|
|
25
|
+
def sign_message(private_key: EllipticCurvePrivateKey, message: bytes) -> str:
|
|
26
|
+
"""Signs a message using the given private key and returns a base64 encoded signature."""
|
|
235
27
|
try:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return
|
|
242
|
-
|
|
243
|
-
if status == True:
|
|
244
|
-
env_path = credentials_folder_path / f"{host}.env"
|
|
245
|
-
if env_path.exists():
|
|
246
|
-
os.remove(env_path)
|
|
247
|
-
click.echo("Credentials deleted successfully!")
|
|
248
|
-
click.echo(f"Neuronum Cell '{host}' has been deleted!")
|
|
249
|
-
else:
|
|
250
|
-
click.echo(f"Neuronum Cell '{host}' deletion failed!")
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
@click.command()
|
|
254
|
-
def init_node():
|
|
255
|
-
descr = click.prompt("Node description: Type up to 25 characters").strip()
|
|
256
|
-
if descr and len(descr) > 25:
|
|
257
|
-
click.echo("Description too long. Max 25 characters allowed.")
|
|
258
|
-
return
|
|
259
|
-
asyncio.run(async_init_node(descr))
|
|
260
|
-
|
|
261
|
-
async def async_init_node(descr):
|
|
262
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
263
|
-
env_path = credentials_folder_path / ".env"
|
|
264
|
-
|
|
265
|
-
env_data = {}
|
|
266
|
-
|
|
267
|
-
try:
|
|
268
|
-
with open(env_path, "r") as f:
|
|
269
|
-
for line in f:
|
|
270
|
-
key, value = line.strip().split("=")
|
|
271
|
-
env_data[key] = value
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
host = env_data.get("HOST", "")
|
|
275
|
-
password = env_data.get("PASSWORD", "")
|
|
276
|
-
network = env_data.get("NETWORK", "")
|
|
277
|
-
synapse = env_data.get("SYNAPSE", "")
|
|
278
|
-
|
|
279
|
-
except FileNotFoundError:
|
|
280
|
-
click.echo("No cell connected. Connect your cell with command neuronum connect-cell")
|
|
281
|
-
return
|
|
28
|
+
signature = private_key.sign(
|
|
29
|
+
message,
|
|
30
|
+
ec.ECDSA(hashes.SHA256())
|
|
31
|
+
)
|
|
32
|
+
return base64.b64encode(signature).decode()
|
|
282
33
|
except Exception as e:
|
|
283
|
-
click.echo(f"Error
|
|
284
|
-
return
|
|
285
|
-
|
|
286
|
-
url = f"https://{network}/api/init_node"
|
|
287
|
-
node = {
|
|
288
|
-
"host": host,
|
|
289
|
-
"password": password,
|
|
290
|
-
"synapse": synapse,
|
|
291
|
-
"descr": descr,
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async with aiohttp.ClientSession() as session:
|
|
295
|
-
try:
|
|
296
|
-
async with session.post(url, json=node) as response:
|
|
297
|
-
response.raise_for_status()
|
|
298
|
-
data = await response.json()
|
|
299
|
-
node_id = data["nodeID"]
|
|
300
|
-
except aiohttp.ClientError as e:
|
|
301
|
-
click.echo(f"Error sending request: {e}")
|
|
302
|
-
return
|
|
303
|
-
|
|
304
|
-
node_filename = descr + "_" + node_id.replace("::node", "")
|
|
305
|
-
project_path = Path(node_filename)
|
|
306
|
-
project_path.mkdir(exist_ok=True)
|
|
34
|
+
click.echo(f"❌ Error signing message: {e}")
|
|
35
|
+
return ""
|
|
307
36
|
|
|
37
|
+
def derive_keys_from_mnemonic(mnemonic: str):
|
|
38
|
+
"""Derives EC-SECP256R1 keys from a BIP-39 mnemonic's seed."""
|
|
308
39
|
try:
|
|
309
|
-
|
|
40
|
+
seed = Bip39SeedGenerator(mnemonic).Generate()
|
|
41
|
+
# Hash the seed to get a deterministic and strong key derivation input
|
|
42
|
+
digest = hashlib.sha256(seed).digest()
|
|
43
|
+
int_key = int.from_bytes(digest, "big")
|
|
44
|
+
|
|
45
|
+
# Derive the private key
|
|
46
|
+
private_key = ec.derive_private_key(int_key, ec.SECP256R1(), default_backend())
|
|
310
47
|
public_key = private_key.public_key()
|
|
311
48
|
|
|
49
|
+
# Serialize keys to PEM format
|
|
312
50
|
pem_private = private_key.private_bytes(
|
|
313
51
|
encoding=serialization.Encoding.PEM,
|
|
314
52
|
format=serialization.PrivateFormat.PKCS8,
|
|
@@ -319,717 +57,263 @@ async def async_init_node(descr):
|
|
|
319
57
|
encoding=serialization.Encoding.PEM,
|
|
320
58
|
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
321
59
|
)
|
|
322
|
-
|
|
323
|
-
public_key_pem_file = project_path / "public_key.pem"
|
|
324
|
-
with open(public_key_pem_file, "wb") as key_file:
|
|
325
|
-
key_file.write(pem_public)
|
|
326
|
-
|
|
327
|
-
private_key_pem_file = project_path / "private_key.pem"
|
|
328
|
-
with open(private_key_pem_file, "wb") as key_file:
|
|
329
|
-
key_file.write(pem_private)
|
|
330
|
-
|
|
331
|
-
pem_public_str = pem_public.decode('utf-8')
|
|
332
|
-
pem_public_oneline = "".join(pem_public_str.split())
|
|
333
|
-
|
|
334
|
-
current_directory = os.getcwd()
|
|
335
|
-
private_key_file = os.path.join(current_directory / project_path, "private_key.pem")
|
|
336
|
-
public_key_file = os.path.join(current_directory / project_path, "public_key.pem")
|
|
337
|
-
except:
|
|
338
|
-
print("Error creating Private/Public Key Pair")
|
|
339
|
-
|
|
340
|
-
app_path = project_path / "app.py"
|
|
341
|
-
app_path.write_text(f"""\
|
|
342
|
-
import asyncio
|
|
343
|
-
from neuronum import Node
|
|
344
|
-
from jinja2 import Environment, FileSystemLoader
|
|
345
|
-
|
|
346
|
-
env = Environment(loader=FileSystemLoader('.'))
|
|
347
|
-
template = env.get_template('ping.html')
|
|
348
|
-
|
|
349
|
-
node = Node(
|
|
350
|
-
id="{node_id}",
|
|
351
|
-
private_key="{private_key_file}",
|
|
352
|
-
public_key="{public_key_file}"
|
|
353
|
-
)
|
|
354
|
-
|
|
355
|
-
async def main():
|
|
356
|
-
|
|
357
|
-
async for transmitter in node.sync():
|
|
358
|
-
ts = transmitter.get("time")
|
|
359
|
-
data = transmitter.get("data")
|
|
360
|
-
transmitter_id = transmitter.get("transmitter_id")
|
|
361
|
-
client = transmitter.get("operator")
|
|
362
|
-
client_public_key = data.get("publicKey", {{}})
|
|
363
|
-
action = data.get("action")
|
|
364
|
-
|
|
365
|
-
response_data = {{}}
|
|
366
|
-
|
|
367
|
-
if action == "ping_node" or action == "start_app":
|
|
368
60
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
response_data = {{
|
|
372
|
-
"json": f"{{transmitter_id}} - Reply from {node_id}: Pinged by {{client}} at {{ts}} with action: {{action}}",
|
|
373
|
-
"html": html_content
|
|
374
|
-
}}
|
|
375
|
-
|
|
376
|
-
if not client_public_key:
|
|
377
|
-
await node.tx_response(transmitter_id, response_data, encrypted=False)
|
|
378
|
-
else:
|
|
379
|
-
await node.tx_response(transmitter_id, response_data, client_public_key)
|
|
380
|
-
|
|
381
|
-
asyncio.run(main())
|
|
382
|
-
""")
|
|
61
|
+
return private_key, public_key, pem_private, pem_public
|
|
383
62
|
|
|
384
|
-
html_path = project_path / "ping.html"
|
|
385
|
-
html_content = f"""\
|
|
386
|
-
<!DOCTYPE html>
|
|
387
|
-
<html>
|
|
388
|
-
<head>
|
|
389
|
-
<style>
|
|
390
|
-
body {{
|
|
391
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
392
|
-
background-color: #121212;
|
|
393
|
-
color: #e0e0e0;
|
|
394
|
-
margin: 0;
|
|
395
|
-
padding: 0;
|
|
396
|
-
display: flex;
|
|
397
|
-
justify-content: center;
|
|
398
|
-
align-items: center;
|
|
399
|
-
min-height: 100vh;
|
|
400
|
-
}}
|
|
401
|
-
|
|
402
|
-
.container {{
|
|
403
|
-
background-color: #1e1e1e;
|
|
404
|
-
border-radius: 12px;
|
|
405
|
-
padding: 40px;
|
|
406
|
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
407
|
-
width: 100%;
|
|
408
|
-
max-width: 500px;
|
|
409
|
-
text-align: center;
|
|
410
|
-
box-sizing: border-box;
|
|
411
|
-
}}
|
|
412
|
-
|
|
413
|
-
.logo {{
|
|
414
|
-
width: 80px;
|
|
415
|
-
margin-bottom: 25px;
|
|
416
|
-
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.1));
|
|
417
|
-
}}
|
|
418
|
-
|
|
419
|
-
h1 {{
|
|
420
|
-
font-size: 1.5em;
|
|
421
|
-
font-weight: 600;
|
|
422
|
-
margin-bottom: 5px;
|
|
423
|
-
color: #f5f5f5;
|
|
424
|
-
}}
|
|
425
|
-
|
|
426
|
-
.subtitle {{
|
|
427
|
-
font-size: 0.9em;
|
|
428
|
-
color: #a0a0a0;
|
|
429
|
-
margin-bottom: 30px;
|
|
430
|
-
}}
|
|
431
|
-
|
|
432
|
-
.data-row {{
|
|
433
|
-
background-color: #2a2a2a;
|
|
434
|
-
padding: 12px 15px;
|
|
435
|
-
border-radius: 8px;
|
|
436
|
-
margin-bottom: 10px;
|
|
437
|
-
display: flex;
|
|
438
|
-
justify-content: space-between;
|
|
439
|
-
align-items: center;
|
|
440
|
-
}}
|
|
441
|
-
|
|
442
|
-
.data-label {{
|
|
443
|
-
font-weight: 400;
|
|
444
|
-
color: #a0a0a0;
|
|
445
|
-
margin: 0;
|
|
446
|
-
}}
|
|
447
|
-
|
|
448
|
-
.data-value {{
|
|
449
|
-
font-weight: 500;
|
|
450
|
-
color: #e0e0e0;
|
|
451
|
-
margin: 0;
|
|
452
|
-
}}
|
|
453
|
-
|
|
454
|
-
.data-value.truncated {{
|
|
455
|
-
white-space: nowrap;
|
|
456
|
-
overflow: hidden;
|
|
457
|
-
text-overflow: ellipsis;
|
|
458
|
-
max-width: 60%;
|
|
459
|
-
}}
|
|
460
|
-
|
|
461
|
-
.data-value.client {{
|
|
462
|
-
color: #8cafff;
|
|
463
|
-
}}
|
|
464
|
-
.data-value.timestamp {{
|
|
465
|
-
color: #a1e8a1;
|
|
466
|
-
}}
|
|
467
|
-
.data-value.transmitter-id {{
|
|
468
|
-
color: #f7a2a2;
|
|
469
|
-
}}
|
|
470
|
-
.api-button {{
|
|
471
|
-
background: #01c07d 100%;
|
|
472
|
-
color: white;
|
|
473
|
-
border: none;
|
|
474
|
-
border-radius: 8px;
|
|
475
|
-
padding: 12px 24px;
|
|
476
|
-
font-size: 16px;
|
|
477
|
-
font-weight: bold;
|
|
478
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
479
|
-
cursor: pointer;
|
|
480
|
-
margin-top: 10px;
|
|
481
|
-
}}
|
|
482
|
-
</style>
|
|
483
|
-
</head>
|
|
484
|
-
<body>
|
|
485
|
-
<div class="container">
|
|
486
|
-
<img class="logo" src="https://neuronum.net/static/logo.png" alt="Neuronum Logo">
|
|
487
|
-
|
|
488
|
-
<h1>Reply from {node_id}</h1>
|
|
489
|
-
<p class="subtitle">Pinged successfully.</p>
|
|
490
|
-
|
|
491
|
-
<div class="data-row">
|
|
492
|
-
<p class="data-label">Client</p>
|
|
493
|
-
<p class="data-value client">{{{{client}}}}</p>
|
|
494
|
-
</div>
|
|
495
|
-
|
|
496
|
-
<div class="data-row">
|
|
497
|
-
<p class="data-label">Timestamp</p>
|
|
498
|
-
<p class="data-value timestamp">{{{{ts}}}}</p>
|
|
499
|
-
</div>
|
|
500
|
-
|
|
501
|
-
<div class="data-row">
|
|
502
|
-
<p class="data-label">Data</p>
|
|
503
|
-
<p class="data-value">{{{{data}}}}</p>
|
|
504
|
-
</div>
|
|
505
|
-
|
|
506
|
-
<div class="data-row">
|
|
507
|
-
<p class="data-label">Transmitter ID</p>
|
|
508
|
-
<p class="data-value transmitter-id truncated">{{{{transmitter_id}}}}</p>
|
|
509
|
-
</div>
|
|
510
|
-
|
|
511
|
-
<button id="send-request-btn" class="api-button">Ping again</button>
|
|
512
|
-
</div>
|
|
513
|
-
|
|
514
|
-
<script>
|
|
515
|
-
document.getElementById('send-request-btn').addEventListener('click', () => {{
|
|
516
|
-
const messagePayload = {{
|
|
517
|
-
type: 'iframe_request',
|
|
518
|
-
endpoint: 'https://neuronum.net/browser/api/activate_tx/{node_id}',
|
|
519
|
-
data: {{ "action": "ping_node" }},
|
|
520
|
-
nodePublicKey: '{pem_public_oneline}',
|
|
521
|
-
}};
|
|
522
|
-
if (window.parent) {{
|
|
523
|
-
window.parent.postMessage(messagePayload, '*');
|
|
524
|
-
}}
|
|
525
|
-
}});
|
|
526
|
-
</script>
|
|
527
|
-
|
|
528
|
-
</body>
|
|
529
|
-
</html>
|
|
530
|
-
"""
|
|
531
|
-
html_path.write_text(html_content)
|
|
532
|
-
config_path = project_path / "config.json"
|
|
533
|
-
await asyncio.to_thread(
|
|
534
|
-
config_path.write_text,
|
|
535
|
-
f"""{{
|
|
536
|
-
"app_metadata": {{
|
|
537
|
-
"name": "{descr}",
|
|
538
|
-
"version": "1.0.0",
|
|
539
|
-
"author": "{host}",
|
|
540
|
-
"audience": "private",
|
|
541
|
-
"logo": "https://neuronum.net/static/logo.png",
|
|
542
|
-
"node_id": "{node_id}"
|
|
543
|
-
}},
|
|
544
|
-
"legals": {{
|
|
545
|
-
"terms": "https://url_to_your/terms",
|
|
546
|
-
"privacy_policy": "https://url_to_your/privacy_policy"
|
|
547
|
-
}},
|
|
548
|
-
"public_key": "{pem_public_oneline}"
|
|
549
|
-
}}"""
|
|
550
|
-
|
|
551
|
-
)
|
|
552
|
-
click.echo(f"Neuronum Node '{node_id}' initialized!")
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
@click.command()
|
|
556
|
-
@click.option('--d', is_flag=True, help="Start node in detached mode")
|
|
557
|
-
def start_node(d):
|
|
558
|
-
update_node_at_start()
|
|
559
|
-
pid_file = Path.cwd() / "status.txt"
|
|
560
|
-
system_name = platform.system()
|
|
561
|
-
active_pids = []
|
|
562
|
-
|
|
563
|
-
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
564
|
-
|
|
565
|
-
if pid_file.exists():
|
|
566
|
-
try:
|
|
567
|
-
with open(pid_file, "r") as f:
|
|
568
|
-
pids = [int(line.strip()) for line in f if line.strip().isdigit()]
|
|
569
|
-
for pid in pids:
|
|
570
|
-
if system_name == "Windows":
|
|
571
|
-
if psutil.pid_exists(pid):
|
|
572
|
-
active_pids.append(pid)
|
|
573
|
-
else:
|
|
574
|
-
try:
|
|
575
|
-
os.kill(pid, 0)
|
|
576
|
-
active_pids.append(pid)
|
|
577
|
-
except OSError:
|
|
578
|
-
continue
|
|
579
|
-
except Exception as e:
|
|
580
|
-
click.echo(f"Failed to read PID file: {e}")
|
|
581
|
-
|
|
582
|
-
if active_pids:
|
|
583
|
-
click.echo(f"Node is already running. Active PIDs: {', '.join(map(str, active_pids))}")
|
|
584
|
-
return
|
|
585
|
-
|
|
586
|
-
click.echo("Starting Node...")
|
|
587
|
-
|
|
588
|
-
project_path = Path.cwd()
|
|
589
|
-
script_files = glob.glob("app.py")
|
|
590
|
-
processes = []
|
|
591
|
-
|
|
592
|
-
for script in script_files:
|
|
593
|
-
script_path = project_path / script
|
|
594
|
-
if script_path.exists():
|
|
595
|
-
|
|
596
|
-
if d:
|
|
597
|
-
process = subprocess.Popen(
|
|
598
|
-
["nohup", sys.executable, str(script_path), "&"] if system_name != "Windows"
|
|
599
|
-
else ["pythonw", str(script_path)],
|
|
600
|
-
stdout=subprocess.DEVNULL,
|
|
601
|
-
stderr=subprocess.DEVNULL,
|
|
602
|
-
start_new_session=True
|
|
603
|
-
)
|
|
604
|
-
else:
|
|
605
|
-
process = subprocess.Popen(
|
|
606
|
-
[sys.executable, str(script_path)]
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
processes.append(process.pid)
|
|
610
|
-
|
|
611
|
-
if not processes:
|
|
612
|
-
click.echo("Error: No valid node script found. Ensure the node is set up correctly.")
|
|
613
|
-
return
|
|
614
|
-
|
|
615
|
-
with open(pid_file, "w") as f:
|
|
616
|
-
f.write(f"Started at: {start_time}\n")
|
|
617
|
-
f.write("\n".join(map(str, processes)))
|
|
618
|
-
|
|
619
|
-
click.echo(f"Node started successfully with PIDs: {', '.join(map(str, processes))}")
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
@click.command()
|
|
623
|
-
def check_node():
|
|
624
|
-
click.echo("Checking Node status...")
|
|
625
|
-
|
|
626
|
-
try:
|
|
627
|
-
with open('config.json', 'r') as f:
|
|
628
|
-
data = json.load(f)
|
|
629
|
-
|
|
630
|
-
nodeID = data['app_metadata']['node_id']
|
|
631
|
-
|
|
632
|
-
except FileNotFoundError:
|
|
633
|
-
click.echo("Error: .env with credentials not found")
|
|
634
|
-
return
|
|
635
63
|
except Exception as e:
|
|
636
|
-
click.echo(f"Error
|
|
637
|
-
return
|
|
64
|
+
click.echo(f"❌ Error generating keys from mnemonic: {e}")
|
|
65
|
+
return None, None, None, None
|
|
638
66
|
|
|
639
|
-
pid_file = Path.cwd() / "status.txt"
|
|
640
|
-
|
|
641
|
-
if not pid_file.exists():
|
|
642
|
-
click.echo(f"Node {nodeID} is not running. Status file missing.")
|
|
643
|
-
return
|
|
644
67
|
|
|
68
|
+
def save_credentials(host: str, mnemonic: str, pem_public: bytes, pem_private: bytes):
|
|
69
|
+
"""Saves host, mnemonic, and keys to the .neuronum directory."""
|
|
645
70
|
try:
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
71
|
+
NEURONUM_PATH.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
# Save .env with host and mnemonic (Sensitive data)
|
|
74
|
+
env_content = f"HOST={host}\nMNEMONIC=\"{mnemonic}\""
|
|
75
|
+
ENV_FILE.write_text(env_content)
|
|
76
|
+
|
|
77
|
+
# Save PEM files
|
|
78
|
+
PUBLIC_KEY_FILE.write_bytes(pem_public)
|
|
79
|
+
PRIVATE_KEY_FILE.write_bytes(pem_private)
|
|
80
|
+
|
|
81
|
+
return True
|
|
657
82
|
except Exception as e:
|
|
658
|
-
click.echo(f"
|
|
659
|
-
return
|
|
83
|
+
click.echo(f"❌ Error saving credentials: {e}")
|
|
84
|
+
return False
|
|
660
85
|
|
|
661
|
-
|
|
662
|
-
|
|
86
|
+
def load_credentials():
|
|
87
|
+
"""Loads host, mnemonic, and private key from local files."""
|
|
88
|
+
credentials = {}
|
|
89
|
+
try:
|
|
90
|
+
# Load .env data (Host and Mnemonic)
|
|
91
|
+
if not ENV_FILE.exists():
|
|
92
|
+
click.echo("Error: No credentials found. Please create or connect a cell first.")
|
|
93
|
+
return None
|
|
663
94
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
running_pids.append(pid)
|
|
672
|
-
except OSError:
|
|
673
|
-
continue
|
|
95
|
+
with open(ENV_FILE, "r") as f:
|
|
96
|
+
for line in f:
|
|
97
|
+
line = line.strip()
|
|
98
|
+
if "=" in line:
|
|
99
|
+
key, value = line.split("=", 1)
|
|
100
|
+
# Clean up quotes from mnemonic
|
|
101
|
+
credentials[key] = value.strip().strip('"')
|
|
674
102
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
103
|
+
credentials['host'] = credentials.get("HOST")
|
|
104
|
+
credentials['mnemonic'] = credentials.get("MNEMONIC")
|
|
105
|
+
|
|
106
|
+
# Load Private Key
|
|
107
|
+
with open(PRIVATE_KEY_FILE, "rb") as key_file:
|
|
108
|
+
private_key = serialization.load_pem_private_key(
|
|
109
|
+
key_file.read(),
|
|
110
|
+
password=None,
|
|
111
|
+
backend=default_backend()
|
|
112
|
+
)
|
|
113
|
+
credentials['private_key'] = private_key
|
|
114
|
+
credentials['public_key'] = private_key.public_key()
|
|
679
115
|
|
|
116
|
+
return credentials
|
|
680
117
|
|
|
681
|
-
@click.command()
|
|
682
|
-
@click.option('--d', is_flag=True, help="Restart node in detached mode")
|
|
683
|
-
def restart_node(d):
|
|
684
|
-
update_node_at_start()
|
|
685
|
-
pid_file = Path.cwd() / "status.txt"
|
|
686
|
-
system_name = platform.system()
|
|
687
|
-
|
|
688
|
-
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
689
|
-
|
|
690
|
-
try:
|
|
691
|
-
with open('config.json', 'r') as f:
|
|
692
|
-
data = json.load(f)
|
|
693
|
-
|
|
694
|
-
nodeID = data['app_metadata']['node_id']
|
|
695
|
-
|
|
696
118
|
except FileNotFoundError:
|
|
697
|
-
|
|
698
|
-
return
|
|
119
|
+
click.echo("Error: Credentials files are incomplete. Try deleting the '.neuronum' folder or reconnecting.")
|
|
120
|
+
return None
|
|
699
121
|
except Exception as e:
|
|
700
|
-
|
|
701
|
-
return
|
|
702
|
-
|
|
703
|
-
if pid_file.exists():
|
|
704
|
-
try:
|
|
705
|
-
with open(pid_file, "r") as f:
|
|
706
|
-
pids = [int(line.strip()) for line in f if line.strip().isdigit()]
|
|
707
|
-
|
|
708
|
-
for pid in pids:
|
|
709
|
-
if system_name == "Windows":
|
|
710
|
-
if psutil.pid_exists(pid):
|
|
711
|
-
proc = psutil.Process(pid)
|
|
712
|
-
proc.terminate()
|
|
713
|
-
else:
|
|
714
|
-
try:
|
|
715
|
-
os.kill(pid, 15)
|
|
716
|
-
except OSError:
|
|
717
|
-
continue
|
|
718
|
-
|
|
719
|
-
pid_file.unlink()
|
|
720
|
-
|
|
721
|
-
click.echo(f"Terminated existing {nodeID} processes: {', '.join(map(str, pids))}")
|
|
722
|
-
|
|
723
|
-
except Exception as e:
|
|
724
|
-
click.echo(f"Failed to terminate processes: {e}")
|
|
725
|
-
return
|
|
726
|
-
else:
|
|
727
|
-
click.echo(f"Node {nodeID} is not running")
|
|
728
|
-
|
|
729
|
-
click.echo(f"Starting Node {nodeID}...")
|
|
730
|
-
project_path = Path.cwd()
|
|
731
|
-
script_files = glob.glob("app.py")
|
|
732
|
-
processes = []
|
|
733
|
-
|
|
734
|
-
for script in script_files:
|
|
735
|
-
script_path = project_path / script
|
|
736
|
-
if script_path.exists():
|
|
737
|
-
if d:
|
|
738
|
-
process = subprocess.Popen(
|
|
739
|
-
["nohup", sys.executable, str(script_path), "&"] if system_name != "Windows"
|
|
740
|
-
else ["pythonw", str(script_path)],
|
|
741
|
-
stdout=subprocess.DEVNULL,
|
|
742
|
-
stderr=subprocess.DEVNULL,
|
|
743
|
-
start_new_session=True
|
|
744
|
-
)
|
|
745
|
-
else:
|
|
746
|
-
process = subprocess.Popen(
|
|
747
|
-
[sys.executable, str(script_path)]
|
|
748
|
-
)
|
|
749
|
-
|
|
750
|
-
processes.append(process.pid)
|
|
751
|
-
|
|
752
|
-
if not processes:
|
|
753
|
-
click.echo("Error: No valid node script found.")
|
|
754
|
-
return
|
|
122
|
+
click.echo(f"Error loading credentials: {e}")
|
|
123
|
+
return None
|
|
755
124
|
|
|
756
|
-
|
|
757
|
-
f.write(f"Started at: {start_time}\n")
|
|
758
|
-
f.write("\n".join(map(str, processes)))
|
|
125
|
+
# --- CLI Group ---
|
|
759
126
|
|
|
760
|
-
|
|
127
|
+
@click.group()
|
|
128
|
+
def cli():
|
|
129
|
+
"""Neuronum CLI Tool for Community Cell management."""
|
|
130
|
+
pass
|
|
761
131
|
|
|
132
|
+
# --- CLI Commands ---
|
|
762
133
|
|
|
763
134
|
@click.command()
|
|
764
|
-
def
|
|
765
|
-
|
|
135
|
+
def create_cell():
|
|
136
|
+
"""Creates a new Community Cell with a randomly generated key pair."""
|
|
137
|
+
|
|
138
|
+
# 1. Generate Mnemonic and Keys
|
|
139
|
+
mnemonic = Bip39MnemonicGenerator().FromWordsNumber(12)
|
|
140
|
+
private_key, public_key, pem_private, pem_public = derive_keys_from_mnemonic(mnemonic)
|
|
766
141
|
|
|
767
|
-
|
|
768
|
-
|
|
142
|
+
if not private_key:
|
|
143
|
+
return
|
|
769
144
|
|
|
770
|
-
|
|
145
|
+
# 2. Call API to Create Cell
|
|
146
|
+
click.echo("🔗 Requesting new cell creation from server...")
|
|
147
|
+
url = f"{API_BASE_URL}/create_cell"
|
|
148
|
+
create_data = {"public_key": pem_public.decode("utf-8")}
|
|
771
149
|
|
|
772
150
|
try:
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
nodeID = data['app_metadata']['node_id']
|
|
151
|
+
response = requests.post(url, json=create_data, timeout=10)
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
host = response.json().get("host")
|
|
777
154
|
|
|
778
|
-
except
|
|
779
|
-
|
|
780
|
-
return
|
|
781
|
-
except Exception as e:
|
|
782
|
-
print(f"Error reading .env file: {e}")
|
|
155
|
+
except requests.exceptions.RequestException as e:
|
|
156
|
+
click.echo(f"❌ Error communicating with the server: {e}")
|
|
783
157
|
return
|
|
784
158
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
click.echo(f"Node {nodeID} stopped successfully!")
|
|
802
|
-
|
|
803
|
-
except FileNotFoundError:
|
|
804
|
-
click.echo("Error: No active node process found.")
|
|
805
|
-
except subprocess.CalledProcessError:
|
|
806
|
-
click.echo("Error: Unable to stop some node processes.")
|
|
159
|
+
# 3. Save Credentials
|
|
160
|
+
if host:
|
|
161
|
+
if save_credentials(host, mnemonic, pem_public, pem_private):
|
|
162
|
+
click.echo("\n" + "=" * 50)
|
|
163
|
+
click.echo(" ✅ WELCOME TO NEURONUM! Cell Created Successfully.")
|
|
164
|
+
click.echo("=" * 50)
|
|
165
|
+
click.echo(f" Host: {host}")
|
|
166
|
+
click.echo(f" Mnemonic (CRITICAL! Back this up!):")
|
|
167
|
+
click.echo(f" {mnemonic}")
|
|
168
|
+
click.echo("-" * 50)
|
|
169
|
+
click.echo(f"Credentials saved to: {NEURONUM_PATH}")
|
|
170
|
+
else:
|
|
171
|
+
# Error saving credentials already echoed in helper
|
|
172
|
+
pass
|
|
173
|
+
else:
|
|
174
|
+
click.echo("❌ Error: Server did not return a host. Cell creation failed.")
|
|
807
175
|
|
|
808
176
|
|
|
809
177
|
@click.command()
|
|
810
|
-
def
|
|
811
|
-
|
|
812
|
-
try:
|
|
813
|
-
env_path = Path.home() / ".neuronum" / ".env"
|
|
814
|
-
env_data = {}
|
|
815
|
-
with open(env_path, "r") as f:
|
|
816
|
-
for line in f:
|
|
817
|
-
if "=" in line:
|
|
818
|
-
key, value = line.strip().split("=", 1)
|
|
819
|
-
env_data[key] = value
|
|
820
|
-
|
|
821
|
-
host = env_data.get("HOST", "")
|
|
822
|
-
|
|
823
|
-
with open("config.json", "r") as f:
|
|
824
|
-
config_data = json.load(f)
|
|
825
|
-
|
|
826
|
-
audience = config_data.get("app_metadata", {}).get("audience", "")
|
|
827
|
-
descr = config_data.get("app_metadata", {}).get("name", "")
|
|
178
|
+
def connect_cell():
|
|
179
|
+
"""Connects to an existing Community Cell using a 12-word mnemonic."""
|
|
828
180
|
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
'Community Cells can only create private Nodes. Set audience to "private".'
|
|
832
|
-
)
|
|
833
|
-
if descr and len(descr) > 25:
|
|
834
|
-
raise click.ClickException(
|
|
835
|
-
'Description too long. Max 25 characters allowed.'
|
|
836
|
-
)
|
|
181
|
+
# 1. Get and Validate Mnemonic
|
|
182
|
+
mnemonic = questionary.text("Enter your 12-word BIP-39 mnemonic (space separated):").ask()
|
|
837
183
|
|
|
838
|
-
|
|
839
|
-
click.echo(
|
|
840
|
-
return
|
|
841
|
-
except click.ClickException as e:
|
|
842
|
-
click.echo(e.format_message())
|
|
843
|
-
return
|
|
844
|
-
except Exception as e:
|
|
845
|
-
click.echo(f"Error reading files: {e}")
|
|
184
|
+
if not mnemonic:
|
|
185
|
+
click.echo("Connection canceled.")
|
|
846
186
|
return
|
|
847
187
|
|
|
848
|
-
|
|
188
|
+
mnemonic = " ".join(mnemonic.strip().split())
|
|
189
|
+
words = mnemonic.split()
|
|
849
190
|
|
|
191
|
+
if len(words) != 12:
|
|
192
|
+
click.echo("❌ Mnemonic must be exactly 12 words.")
|
|
193
|
+
return
|
|
850
194
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
password = env_data.get("PASSWORD", "")
|
|
855
|
-
network = env_data.get("NETWORK", "")
|
|
856
|
-
synapse = env_data.get("SYNAPSE", "")
|
|
857
|
-
|
|
858
|
-
node_id = config_data.get("app_metadata", [{}]).get("node_id", "")
|
|
859
|
-
|
|
860
|
-
with open("config.json", "r") as f:
|
|
861
|
-
config_file_content = f.read()
|
|
195
|
+
if not Bip39MnemonicValidator(Bip39Languages.ENGLISH).IsValid(mnemonic):
|
|
196
|
+
click.echo("❌ Invalid mnemonic. Please ensure all words are valid BIP-39 words.")
|
|
197
|
+
return
|
|
862
198
|
|
|
863
|
-
|
|
864
|
-
|
|
199
|
+
# 2. Derive Keys
|
|
200
|
+
private_key, public_key, pem_private, pem_public = derive_keys_from_mnemonic(mnemonic)
|
|
201
|
+
if not private_key:
|
|
865
202
|
return
|
|
866
|
-
|
|
867
|
-
|
|
203
|
+
|
|
204
|
+
# 3. Prepare Signed Message
|
|
205
|
+
timestamp = str(int(time.time()))
|
|
206
|
+
public_key_pem_str = pem_public.decode('utf-8')
|
|
207
|
+
message = f"public_key={public_key_pem_str};timestamp={timestamp}"
|
|
208
|
+
signature_b64 = sign_message(private_key, message.encode())
|
|
209
|
+
|
|
210
|
+
if not signature_b64:
|
|
868
211
|
return
|
|
869
212
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
"
|
|
875
|
-
"
|
|
876
|
-
"
|
|
877
|
-
"config_file": config_file_content,
|
|
878
|
-
"descr": descr,
|
|
213
|
+
# 4. Call API to Connect
|
|
214
|
+
click.echo("🔗 Attempting to connect to cell...")
|
|
215
|
+
url = f"{API_BASE_URL}/connect_cell"
|
|
216
|
+
connect_data = {
|
|
217
|
+
"public_key": public_key_pem_str,
|
|
218
|
+
"signed_message": signature_b64,
|
|
219
|
+
"message": message
|
|
879
220
|
}
|
|
880
221
|
|
|
881
|
-
async with aiohttp.ClientSession() as session:
|
|
882
|
-
try:
|
|
883
|
-
async with session.post(url, json=node) as response:
|
|
884
|
-
response.raise_for_status()
|
|
885
|
-
data = await response.json()
|
|
886
|
-
updated_node_id = data.get("nodeID", node_id)
|
|
887
|
-
click.echo(f"Neuronum Node '{updated_node_id}' updated!")
|
|
888
|
-
except aiohttp.ClientError as e:
|
|
889
|
-
click.echo(f"Error sending request: {e}")
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
def update_node_at_start():
|
|
894
|
-
click.echo("Update your Node")
|
|
895
222
|
try:
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
env_data[key] = value
|
|
223
|
+
response = requests.post(url, json=connect_data, timeout=10)
|
|
224
|
+
response.raise_for_status()
|
|
225
|
+
host = response.json().get("host")
|
|
226
|
+
except requests.exceptions.RequestException as e:
|
|
227
|
+
click.echo(f"❌ Error connecting to cell: {e}")
|
|
228
|
+
return
|
|
903
229
|
|
|
904
|
-
|
|
905
|
-
|
|
230
|
+
# 5. Save Credentials
|
|
231
|
+
if host:
|
|
232
|
+
if save_credentials(host, mnemonic, pem_public, pem_private):
|
|
233
|
+
click.echo(f"🔗 Successfully connected to Community Cell '{host}'.")
|
|
234
|
+
# Error saving credentials already echoed in helper
|
|
235
|
+
else:
|
|
236
|
+
click.echo("❌ Failed to retrieve host from server. Connection failed.")
|
|
906
237
|
|
|
907
|
-
host = env_data.get("HOST", "")
|
|
908
|
-
audience = config_data.get("app_metadata", {}).get("audience", "")
|
|
909
|
-
descr = config_data.get("app_metadata", {}).get("name", "")
|
|
910
238
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
239
|
+
@click.command()
|
|
240
|
+
def view_cell():
|
|
241
|
+
"""Displays the connection status and host name of the current cell."""
|
|
242
|
+
|
|
243
|
+
credentials = load_credentials()
|
|
244
|
+
|
|
245
|
+
if credentials:
|
|
246
|
+
click.echo("\n--- Neuronum Cell Status ---")
|
|
247
|
+
click.echo(f"Status: ✅ Connected")
|
|
248
|
+
click.echo(f"Host: {credentials['host']}")
|
|
249
|
+
click.echo(f"Path: {NEURONUM_PATH}")
|
|
250
|
+
click.echo(f"Key Type: {credentials['private_key'].curve.name} (SECP256R1)")
|
|
251
|
+
click.echo("----------------------------")
|
|
919
252
|
|
|
920
|
-
asyncio.run(_async_update_node_at_start(env_data, config_data, audience, descr))
|
|
921
253
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
254
|
+
@click.command()
|
|
255
|
+
def delete_cell():
|
|
256
|
+
"""Deletes the locally stored credentials and requests cell deletion from the server."""
|
|
257
|
+
|
|
258
|
+
# 1. Load Credentials
|
|
259
|
+
credentials = load_credentials()
|
|
260
|
+
if not credentials:
|
|
261
|
+
# Error already echoed in helper
|
|
262
|
+
return
|
|
928
263
|
|
|
264
|
+
host = credentials['host']
|
|
265
|
+
private_key = credentials['private_key']
|
|
929
266
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
267
|
+
# 2. Confirmation
|
|
268
|
+
confirm = click.confirm(f"Are you sure you want to permanently delete connection to '{host}'?", default=False)
|
|
269
|
+
if not confirm:
|
|
270
|
+
click.echo("Deletion canceled.")
|
|
271
|
+
return
|
|
935
272
|
|
|
936
|
-
|
|
273
|
+
# 3. Prepare Signed Message
|
|
274
|
+
timestamp = str(int(time.time()))
|
|
275
|
+
message = f"host={host};timestamp={timestamp}"
|
|
276
|
+
signature_b64 = sign_message(private_key, message.encode())
|
|
937
277
|
|
|
938
|
-
|
|
939
|
-
with open("config.json", "r") as f:
|
|
940
|
-
config_file_content = f.read()
|
|
941
|
-
except Exception as e:
|
|
942
|
-
click.echo(f"Error reading config.json content: {e}")
|
|
278
|
+
if not signature_b64:
|
|
943
279
|
return
|
|
944
280
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
281
|
+
# 4. Call API to Delete
|
|
282
|
+
click.echo(f"🗑️ Requesting deletion of cell '{host}'...")
|
|
283
|
+
url = f"{API_BASE_URL}/delete_cell"
|
|
284
|
+
payload = {
|
|
948
285
|
"host": host,
|
|
949
|
-
"
|
|
950
|
-
"
|
|
951
|
-
"node_type": audience,
|
|
952
|
-
"config_file": config_file_content,
|
|
953
|
-
"descr": descr,
|
|
286
|
+
"signed_message": signature_b64,
|
|
287
|
+
"message": message
|
|
954
288
|
}
|
|
955
289
|
|
|
956
|
-
async with aiohttp.ClientSession() as session:
|
|
957
|
-
try:
|
|
958
|
-
async with session.post(url, json=node) as response:
|
|
959
|
-
response.raise_for_status()
|
|
960
|
-
data = await response.json()
|
|
961
|
-
updated_node_id = data.get("nodeID", node_id)
|
|
962
|
-
click.echo(f"Neuronum Node '{updated_node_id}' updated!")
|
|
963
|
-
except aiohttp.ClientError as e:
|
|
964
|
-
click.echo(f"Error sending request: {e}")
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
@click.command()
|
|
968
|
-
def delete_node():
|
|
969
|
-
asyncio.run(async_delete_node())
|
|
970
|
-
|
|
971
|
-
async def async_delete_node():
|
|
972
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
973
|
-
env_path = credentials_folder_path / ".env"
|
|
974
|
-
env_data = {}
|
|
975
|
-
|
|
976
290
|
try:
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
host = env_data.get("HOST", "")
|
|
983
|
-
password = env_data.get("PASSWORD", "")
|
|
984
|
-
network = env_data.get("NETWORK", "")
|
|
985
|
-
synapse = env_data.get("SYNAPSE", "")
|
|
986
|
-
|
|
987
|
-
with open('config.json', 'r') as f:
|
|
988
|
-
data = json.load(f)
|
|
989
|
-
|
|
990
|
-
nodeID = data['app_metadata']['node_id']
|
|
991
|
-
|
|
992
|
-
except FileNotFoundError:
|
|
993
|
-
click.echo("Error: .env with credentials not found")
|
|
994
|
-
return
|
|
995
|
-
except Exception as e:
|
|
996
|
-
click.echo(f"Error reading .env file: {e}")
|
|
291
|
+
response = requests.delete(url, json=payload, timeout=10)
|
|
292
|
+
response.raise_for_status()
|
|
293
|
+
status = response.json().get("status", False)
|
|
294
|
+
except requests.exceptions.RequestException as e:
|
|
295
|
+
click.echo(f"❌ Error communicating with the server during deletion: {e}")
|
|
997
296
|
return
|
|
998
297
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
"nodeID": nodeID,
|
|
1002
|
-
"host": host,
|
|
1003
|
-
"password": password,
|
|
1004
|
-
"synapse": synapse
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
async with aiohttp.ClientSession() as session:
|
|
298
|
+
# 5. Cleanup Local Files
|
|
299
|
+
if status:
|
|
1008
300
|
try:
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
301
|
+
ENV_FILE.unlink(missing_ok=True)
|
|
302
|
+
PRIVATE_KEY_FILE.unlink(missing_ok=True)
|
|
303
|
+
PUBLIC_KEY_FILE.unlink(missing_ok=True)
|
|
304
|
+
|
|
305
|
+
click.echo(f"✅ Neuronum Cell '{host}' has been deleted and local credentials removed.")
|
|
306
|
+
except Exception as e:
|
|
307
|
+
click.echo(f"⚠️ Warning: Successfully deleted cell on server, but failed to clean up all local files: {e}")
|
|
308
|
+
else:
|
|
309
|
+
click.echo(f"❌ Neuronum Cell '{host}' deletion failed on server.")
|
|
1018
310
|
|
|
1019
311
|
|
|
312
|
+
# --- CLI Registration ---
|
|
1020
313
|
cli.add_command(create_cell)
|
|
1021
|
-
cli.add_command(connect_cell)
|
|
1022
314
|
cli.add_command(view_cell)
|
|
1023
|
-
cli.add_command(
|
|
315
|
+
cli.add_command(connect_cell)
|
|
1024
316
|
cli.add_command(delete_cell)
|
|
1025
|
-
cli.add_command(init_node)
|
|
1026
|
-
cli.add_command(update_node)
|
|
1027
|
-
cli.add_command(start_node)
|
|
1028
|
-
cli.add_command(restart_node)
|
|
1029
|
-
cli.add_command(stop_node)
|
|
1030
|
-
cli.add_command(check_node)
|
|
1031
|
-
cli.add_command(delete_node)
|
|
1032
|
-
|
|
1033
317
|
|
|
1034
318
|
if __name__ == "__main__":
|
|
1035
319
|
cli()
|