neuronum 8.4.0__py3-none-any.whl → 10.0.0__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 -942
- neuronum/__init__.py +1 -1
- neuronum/neuronum.py +671 -264
- neuronum-10.0.0.dist-info/METADATA +157 -0
- neuronum-10.0.0.dist-info/RECORD +10 -0
- neuronum-8.4.0.dist-info/METADATA +0 -124
- neuronum-8.4.0.dist-info/RECORD +0 -10
- {neuronum-8.4.0.dist-info → neuronum-10.0.0.dist-info}/WHEEL +0 -0
- {neuronum-8.4.0.dist-info → neuronum-10.0.0.dist-info}/entry_points.txt +0 -0
- {neuronum-8.4.0.dist-info → neuronum-10.0.0.dist-info}/licenses/LICENSE.md +0 -0
- {neuronum-8.4.0.dist-info → neuronum-10.0.0.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,714 +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
|
-
await node.tx_response(transmitter_id, response_data, client_public_key)
|
|
377
|
-
|
|
378
|
-
asyncio.run(main())
|
|
379
|
-
""")
|
|
61
|
+
return private_key, public_key, pem_private, pem_public
|
|
380
62
|
|
|
381
|
-
html_path = project_path / "ping.html"
|
|
382
|
-
html_content = f"""\
|
|
383
|
-
<!DOCTYPE html>
|
|
384
|
-
<html>
|
|
385
|
-
<head>
|
|
386
|
-
<style>
|
|
387
|
-
body {{
|
|
388
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
389
|
-
background-color: #121212;
|
|
390
|
-
color: #e0e0e0;
|
|
391
|
-
margin: 0;
|
|
392
|
-
padding: 0;
|
|
393
|
-
display: flex;
|
|
394
|
-
justify-content: center;
|
|
395
|
-
align-items: center;
|
|
396
|
-
min-height: 100vh;
|
|
397
|
-
}}
|
|
398
|
-
|
|
399
|
-
.container {{
|
|
400
|
-
background-color: #1e1e1e;
|
|
401
|
-
border-radius: 12px;
|
|
402
|
-
padding: 40px;
|
|
403
|
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
404
|
-
width: 100%;
|
|
405
|
-
max-width: 500px;
|
|
406
|
-
text-align: center;
|
|
407
|
-
box-sizing: border-box;
|
|
408
|
-
}}
|
|
409
|
-
|
|
410
|
-
.logo {{
|
|
411
|
-
width: 80px;
|
|
412
|
-
margin-bottom: 25px;
|
|
413
|
-
filter: drop-shadow(0 0 5px rgba(255, 255, 255, 0.1));
|
|
414
|
-
}}
|
|
415
|
-
|
|
416
|
-
h1 {{
|
|
417
|
-
font-size: 1.5em;
|
|
418
|
-
font-weight: 600;
|
|
419
|
-
margin-bottom: 5px;
|
|
420
|
-
color: #f5f5f5;
|
|
421
|
-
}}
|
|
422
|
-
|
|
423
|
-
.subtitle {{
|
|
424
|
-
font-size: 0.9em;
|
|
425
|
-
color: #a0a0a0;
|
|
426
|
-
margin-bottom: 30px;
|
|
427
|
-
}}
|
|
428
|
-
|
|
429
|
-
.data-row {{
|
|
430
|
-
background-color: #2a2a2a;
|
|
431
|
-
padding: 12px 15px;
|
|
432
|
-
border-radius: 8px;
|
|
433
|
-
margin-bottom: 10px;
|
|
434
|
-
display: flex;
|
|
435
|
-
justify-content: space-between;
|
|
436
|
-
align-items: center;
|
|
437
|
-
}}
|
|
438
|
-
|
|
439
|
-
.data-label {{
|
|
440
|
-
font-weight: 400;
|
|
441
|
-
color: #a0a0a0;
|
|
442
|
-
margin: 0;
|
|
443
|
-
}}
|
|
444
|
-
|
|
445
|
-
.data-value {{
|
|
446
|
-
font-weight: 500;
|
|
447
|
-
color: #e0e0e0;
|
|
448
|
-
margin: 0;
|
|
449
|
-
}}
|
|
450
|
-
|
|
451
|
-
.data-value.truncated {{
|
|
452
|
-
white-space: nowrap;
|
|
453
|
-
overflow: hidden;
|
|
454
|
-
text-overflow: ellipsis;
|
|
455
|
-
max-width: 60%;
|
|
456
|
-
}}
|
|
457
|
-
|
|
458
|
-
.data-value.client {{
|
|
459
|
-
color: #8cafff;
|
|
460
|
-
}}
|
|
461
|
-
.data-value.timestamp {{
|
|
462
|
-
color: #a1e8a1;
|
|
463
|
-
}}
|
|
464
|
-
.data-value.transmitter-id {{
|
|
465
|
-
color: #f7a2a2;
|
|
466
|
-
}}
|
|
467
|
-
.api-button {{
|
|
468
|
-
background: #01c07d 100%;
|
|
469
|
-
color: white;
|
|
470
|
-
border: none;
|
|
471
|
-
border-radius: 8px;
|
|
472
|
-
padding: 12px 24px;
|
|
473
|
-
font-size: 16px;
|
|
474
|
-
font-weight: bold;
|
|
475
|
-
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
476
|
-
cursor: pointer;
|
|
477
|
-
margin-top: 10px;
|
|
478
|
-
}}
|
|
479
|
-
</style>
|
|
480
|
-
</head>
|
|
481
|
-
<body>
|
|
482
|
-
<div class="container">
|
|
483
|
-
<img class="logo" src="https://neuronum.net/static/logo.png" alt="Neuronum Logo">
|
|
484
|
-
|
|
485
|
-
<h1>Reply from {node_id}</h1>
|
|
486
|
-
<p class="subtitle">Pinged successfully.</p>
|
|
487
|
-
|
|
488
|
-
<div class="data-row">
|
|
489
|
-
<p class="data-label">Client</p>
|
|
490
|
-
<p class="data-value client">{{{{client}}}}</p>
|
|
491
|
-
</div>
|
|
492
|
-
|
|
493
|
-
<div class="data-row">
|
|
494
|
-
<p class="data-label">Timestamp</p>
|
|
495
|
-
<p class="data-value timestamp">{{{{ts}}}}</p>
|
|
496
|
-
</div>
|
|
497
|
-
|
|
498
|
-
<div class="data-row">
|
|
499
|
-
<p class="data-label">Data</p>
|
|
500
|
-
<p class="data-value">{{{{data}}}}</p>
|
|
501
|
-
</div>
|
|
502
|
-
|
|
503
|
-
<div class="data-row">
|
|
504
|
-
<p class="data-label">Transmitter ID</p>
|
|
505
|
-
<p class="data-value transmitter-id truncated">{{{{transmitter_id}}}}</p>
|
|
506
|
-
</div>
|
|
507
|
-
|
|
508
|
-
<button id="send-request-btn" class="api-button">Ping again</button>
|
|
509
|
-
</div>
|
|
510
|
-
|
|
511
|
-
<script>
|
|
512
|
-
document.getElementById('send-request-btn').addEventListener('click', () => {{
|
|
513
|
-
const messagePayload = {{
|
|
514
|
-
type: 'iframe_request',
|
|
515
|
-
endpoint: 'https://neuronum.net/browser/api/activate_tx/{node_id}',
|
|
516
|
-
data: {{ "action": "ping_node" }},
|
|
517
|
-
nodePublicKey: '{pem_public_oneline}',
|
|
518
|
-
}};
|
|
519
|
-
if (window.parent) {{
|
|
520
|
-
window.parent.postMessage(messagePayload, '*');
|
|
521
|
-
}}
|
|
522
|
-
}});
|
|
523
|
-
</script>
|
|
524
|
-
|
|
525
|
-
</body>
|
|
526
|
-
</html>
|
|
527
|
-
"""
|
|
528
|
-
html_path.write_text(html_content)
|
|
529
|
-
config_path = project_path / "config.json"
|
|
530
|
-
await asyncio.to_thread(
|
|
531
|
-
config_path.write_text,
|
|
532
|
-
f"""{{
|
|
533
|
-
"app_metadata": {{
|
|
534
|
-
"name": "{descr}",
|
|
535
|
-
"version": "1.0.0",
|
|
536
|
-
"author": "{host}",
|
|
537
|
-
"audience": "private",
|
|
538
|
-
"logo": "https://neuronum.net/static/logo.png",
|
|
539
|
-
"node_id": "{node_id}"
|
|
540
|
-
}},
|
|
541
|
-
"legals": {{
|
|
542
|
-
"terms": "https://url_to_your/terms",
|
|
543
|
-
"privacy_policy": "https://url_to_your/privacy_policy"
|
|
544
|
-
}},
|
|
545
|
-
"public_key": "{pem_public_oneline}"
|
|
546
|
-
}}"""
|
|
547
|
-
|
|
548
|
-
)
|
|
549
|
-
click.echo(f"Neuronum Node '{node_id}' initialized!")
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
@click.command()
|
|
553
|
-
@click.option('--d', is_flag=True, help="Start node in detached mode")
|
|
554
|
-
def start_node(d):
|
|
555
|
-
update_node_at_start()
|
|
556
|
-
pid_file = Path.cwd() / "status.txt"
|
|
557
|
-
system_name = platform.system()
|
|
558
|
-
active_pids = []
|
|
559
|
-
|
|
560
|
-
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
561
|
-
|
|
562
|
-
if pid_file.exists():
|
|
563
|
-
try:
|
|
564
|
-
with open(pid_file, "r") as f:
|
|
565
|
-
pids = [int(line.strip()) for line in f if line.strip().isdigit()]
|
|
566
|
-
for pid in pids:
|
|
567
|
-
if system_name == "Windows":
|
|
568
|
-
if psutil.pid_exists(pid):
|
|
569
|
-
active_pids.append(pid)
|
|
570
|
-
else:
|
|
571
|
-
try:
|
|
572
|
-
os.kill(pid, 0)
|
|
573
|
-
active_pids.append(pid)
|
|
574
|
-
except OSError:
|
|
575
|
-
continue
|
|
576
|
-
except Exception as e:
|
|
577
|
-
click.echo(f"Failed to read PID file: {e}")
|
|
578
|
-
|
|
579
|
-
if active_pids:
|
|
580
|
-
click.echo(f"Node is already running. Active PIDs: {', '.join(map(str, active_pids))}")
|
|
581
|
-
return
|
|
582
|
-
|
|
583
|
-
click.echo("Starting Node...")
|
|
584
|
-
|
|
585
|
-
project_path = Path.cwd()
|
|
586
|
-
script_files = glob.glob("app.py")
|
|
587
|
-
processes = []
|
|
588
|
-
|
|
589
|
-
for script in script_files:
|
|
590
|
-
script_path = project_path / script
|
|
591
|
-
if script_path.exists():
|
|
592
|
-
|
|
593
|
-
if d:
|
|
594
|
-
process = subprocess.Popen(
|
|
595
|
-
["nohup", sys.executable, str(script_path), "&"] if system_name != "Windows"
|
|
596
|
-
else ["pythonw", str(script_path)],
|
|
597
|
-
stdout=subprocess.DEVNULL,
|
|
598
|
-
stderr=subprocess.DEVNULL,
|
|
599
|
-
start_new_session=True
|
|
600
|
-
)
|
|
601
|
-
else:
|
|
602
|
-
process = subprocess.Popen(
|
|
603
|
-
[sys.executable, str(script_path)]
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
processes.append(process.pid)
|
|
607
|
-
|
|
608
|
-
if not processes:
|
|
609
|
-
click.echo("Error: No valid node script found. Ensure the node is set up correctly.")
|
|
610
|
-
return
|
|
611
|
-
|
|
612
|
-
with open(pid_file, "w") as f:
|
|
613
|
-
f.write(f"Started at: {start_time}\n")
|
|
614
|
-
f.write("\n".join(map(str, processes)))
|
|
615
|
-
|
|
616
|
-
click.echo(f"Node started successfully with PIDs: {', '.join(map(str, processes))}")
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
@click.command()
|
|
620
|
-
def check_node():
|
|
621
|
-
click.echo("Checking Node status...")
|
|
622
|
-
|
|
623
|
-
try:
|
|
624
|
-
with open('config.json', 'r') as f:
|
|
625
|
-
data = json.load(f)
|
|
626
|
-
|
|
627
|
-
nodeID = data['app_metadata']['node_id']
|
|
628
|
-
|
|
629
|
-
except FileNotFoundError:
|
|
630
|
-
click.echo("Error: .env with credentials not found")
|
|
631
|
-
return
|
|
632
63
|
except Exception as e:
|
|
633
|
-
click.echo(f"Error
|
|
634
|
-
return
|
|
64
|
+
click.echo(f"❌ Error generating keys from mnemonic: {e}")
|
|
65
|
+
return None, None, None, None
|
|
635
66
|
|
|
636
|
-
pid_file = Path.cwd() / "status.txt"
|
|
637
|
-
|
|
638
|
-
if not pid_file.exists():
|
|
639
|
-
click.echo(f"Node {nodeID} is not running. Status file missing.")
|
|
640
|
-
return
|
|
641
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."""
|
|
642
70
|
try:
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|
|
654
82
|
except Exception as e:
|
|
655
|
-
click.echo(f"
|
|
656
|
-
return
|
|
83
|
+
click.echo(f"❌ Error saving credentials: {e}")
|
|
84
|
+
return False
|
|
657
85
|
|
|
658
|
-
|
|
659
|
-
|
|
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
|
|
660
94
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
running_pids.append(pid)
|
|
669
|
-
except OSError:
|
|
670
|
-
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('"')
|
|
671
102
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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()
|
|
676
115
|
|
|
116
|
+
return credentials
|
|
677
117
|
|
|
678
|
-
@click.command()
|
|
679
|
-
@click.option('--d', is_flag=True, help="Restart node in detached mode")
|
|
680
|
-
def restart_node(d):
|
|
681
|
-
update_node_at_start()
|
|
682
|
-
pid_file = Path.cwd() / "status.txt"
|
|
683
|
-
system_name = platform.system()
|
|
684
|
-
|
|
685
|
-
start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
686
|
-
|
|
687
|
-
try:
|
|
688
|
-
with open('config.json', 'r') as f:
|
|
689
|
-
data = json.load(f)
|
|
690
|
-
|
|
691
|
-
nodeID = data['app_metadata']['node_id']
|
|
692
|
-
|
|
693
118
|
except FileNotFoundError:
|
|
694
|
-
|
|
695
|
-
return
|
|
119
|
+
click.echo("Error: Credentials files are incomplete. Try deleting the '.neuronum' folder or reconnecting.")
|
|
120
|
+
return None
|
|
696
121
|
except Exception as e:
|
|
697
|
-
|
|
698
|
-
return
|
|
699
|
-
|
|
700
|
-
if pid_file.exists():
|
|
701
|
-
try:
|
|
702
|
-
with open(pid_file, "r") as f:
|
|
703
|
-
pids = [int(line.strip()) for line in f if line.strip().isdigit()]
|
|
704
|
-
|
|
705
|
-
for pid in pids:
|
|
706
|
-
if system_name == "Windows":
|
|
707
|
-
if psutil.pid_exists(pid):
|
|
708
|
-
proc = psutil.Process(pid)
|
|
709
|
-
proc.terminate()
|
|
710
|
-
else:
|
|
711
|
-
try:
|
|
712
|
-
os.kill(pid, 15)
|
|
713
|
-
except OSError:
|
|
714
|
-
continue
|
|
715
|
-
|
|
716
|
-
pid_file.unlink()
|
|
717
|
-
|
|
718
|
-
click.echo(f"Terminated existing {nodeID} processes: {', '.join(map(str, pids))}")
|
|
719
|
-
|
|
720
|
-
except Exception as e:
|
|
721
|
-
click.echo(f"Failed to terminate processes: {e}")
|
|
722
|
-
return
|
|
723
|
-
else:
|
|
724
|
-
click.echo(f"Node {nodeID} is not running")
|
|
725
|
-
|
|
726
|
-
click.echo(f"Starting Node {nodeID}...")
|
|
727
|
-
project_path = Path.cwd()
|
|
728
|
-
script_files = glob.glob("app.py")
|
|
729
|
-
processes = []
|
|
730
|
-
|
|
731
|
-
for script in script_files:
|
|
732
|
-
script_path = project_path / script
|
|
733
|
-
if script_path.exists():
|
|
734
|
-
if d:
|
|
735
|
-
process = subprocess.Popen(
|
|
736
|
-
["nohup", sys.executable, str(script_path), "&"] if system_name != "Windows"
|
|
737
|
-
else ["pythonw", str(script_path)],
|
|
738
|
-
stdout=subprocess.DEVNULL,
|
|
739
|
-
stderr=subprocess.DEVNULL,
|
|
740
|
-
start_new_session=True
|
|
741
|
-
)
|
|
742
|
-
else:
|
|
743
|
-
process = subprocess.Popen(
|
|
744
|
-
[sys.executable, str(script_path)]
|
|
745
|
-
)
|
|
746
|
-
|
|
747
|
-
processes.append(process.pid)
|
|
748
|
-
|
|
749
|
-
if not processes:
|
|
750
|
-
click.echo("Error: No valid node script found.")
|
|
751
|
-
return
|
|
122
|
+
click.echo(f"Error loading credentials: {e}")
|
|
123
|
+
return None
|
|
752
124
|
|
|
753
|
-
|
|
754
|
-
f.write(f"Started at: {start_time}\n")
|
|
755
|
-
f.write("\n".join(map(str, processes)))
|
|
125
|
+
# --- CLI Group ---
|
|
756
126
|
|
|
757
|
-
|
|
127
|
+
@click.group()
|
|
128
|
+
def cli():
|
|
129
|
+
"""Neuronum CLI Tool for Community Cell management."""
|
|
130
|
+
pass
|
|
758
131
|
|
|
132
|
+
# --- CLI Commands ---
|
|
759
133
|
|
|
760
134
|
@click.command()
|
|
761
|
-
def
|
|
762
|
-
|
|
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)
|
|
763
141
|
|
|
764
|
-
|
|
765
|
-
|
|
142
|
+
if not private_key:
|
|
143
|
+
return
|
|
766
144
|
|
|
767
|
-
|
|
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")}
|
|
768
149
|
|
|
769
150
|
try:
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
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")
|
|
774
154
|
|
|
775
|
-
except
|
|
776
|
-
|
|
777
|
-
return
|
|
778
|
-
except Exception as e:
|
|
779
|
-
print(f"Error reading .env file: {e}")
|
|
155
|
+
except requests.exceptions.RequestException as e:
|
|
156
|
+
click.echo(f"❌ Error communicating with the server: {e}")
|
|
780
157
|
return
|
|
781
158
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
click.echo(f"Node {nodeID} stopped successfully!")
|
|
799
|
-
|
|
800
|
-
except FileNotFoundError:
|
|
801
|
-
click.echo("Error: No active node process found.")
|
|
802
|
-
except subprocess.CalledProcessError:
|
|
803
|
-
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.")
|
|
804
175
|
|
|
805
176
|
|
|
806
177
|
@click.command()
|
|
807
|
-
def
|
|
808
|
-
|
|
809
|
-
try:
|
|
810
|
-
env_path = Path.home() / ".neuronum" / ".env"
|
|
811
|
-
env_data = {}
|
|
812
|
-
with open(env_path, "r") as f:
|
|
813
|
-
for line in f:
|
|
814
|
-
if "=" in line:
|
|
815
|
-
key, value = line.strip().split("=", 1)
|
|
816
|
-
env_data[key] = value
|
|
817
|
-
|
|
818
|
-
host = env_data.get("HOST", "")
|
|
819
|
-
|
|
820
|
-
with open("config.json", "r") as f:
|
|
821
|
-
config_data = json.load(f)
|
|
822
|
-
|
|
823
|
-
audience = config_data.get("app_metadata", {}).get("audience", "")
|
|
824
|
-
descr = config_data.get("app_metadata", {}).get("name", "")
|
|
178
|
+
def connect_cell():
|
|
179
|
+
"""Connects to an existing Community Cell using a 12-word mnemonic."""
|
|
825
180
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
'Community Cells can only create private Nodes. Set audience to "private".'
|
|
829
|
-
)
|
|
830
|
-
if descr and len(descr) > 25:
|
|
831
|
-
raise click.ClickException(
|
|
832
|
-
'Description too long. Max 25 characters allowed.'
|
|
833
|
-
)
|
|
181
|
+
# 1. Get and Validate Mnemonic
|
|
182
|
+
mnemonic = questionary.text("Enter your 12-word BIP-39 mnemonic (space separated):").ask()
|
|
834
183
|
|
|
835
|
-
|
|
836
|
-
click.echo(
|
|
837
|
-
return
|
|
838
|
-
except click.ClickException as e:
|
|
839
|
-
click.echo(e.format_message())
|
|
840
|
-
return
|
|
841
|
-
except Exception as e:
|
|
842
|
-
click.echo(f"Error reading files: {e}")
|
|
184
|
+
if not mnemonic:
|
|
185
|
+
click.echo("Connection canceled.")
|
|
843
186
|
return
|
|
844
187
|
|
|
845
|
-
|
|
188
|
+
mnemonic = " ".join(mnemonic.strip().split())
|
|
189
|
+
words = mnemonic.split()
|
|
846
190
|
|
|
191
|
+
if len(words) != 12:
|
|
192
|
+
click.echo("❌ Mnemonic must be exactly 12 words.")
|
|
193
|
+
return
|
|
847
194
|
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
password = env_data.get("PASSWORD", "")
|
|
852
|
-
network = env_data.get("NETWORK", "")
|
|
853
|
-
synapse = env_data.get("SYNAPSE", "")
|
|
854
|
-
|
|
855
|
-
node_id = config_data.get("app_metadata", [{}]).get("node_id", "")
|
|
856
|
-
|
|
857
|
-
with open("config.json", "r") as f:
|
|
858
|
-
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
|
|
859
198
|
|
|
860
|
-
|
|
861
|
-
|
|
199
|
+
# 2. Derive Keys
|
|
200
|
+
private_key, public_key, pem_private, pem_public = derive_keys_from_mnemonic(mnemonic)
|
|
201
|
+
if not private_key:
|
|
862
202
|
return
|
|
863
|
-
|
|
864
|
-
|
|
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:
|
|
865
211
|
return
|
|
866
212
|
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
"
|
|
872
|
-
"
|
|
873
|
-
"
|
|
874
|
-
"config_file": config_file_content,
|
|
875
|
-
"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
|
|
876
220
|
}
|
|
877
221
|
|
|
878
|
-
async with aiohttp.ClientSession() as session:
|
|
879
|
-
try:
|
|
880
|
-
async with session.post(url, json=node) as response:
|
|
881
|
-
response.raise_for_status()
|
|
882
|
-
data = await response.json()
|
|
883
|
-
updated_node_id = data.get("nodeID", node_id)
|
|
884
|
-
click.echo(f"Neuronum Node '{updated_node_id}' updated!")
|
|
885
|
-
except aiohttp.ClientError as e:
|
|
886
|
-
click.echo(f"Error sending request: {e}")
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
def update_node_at_start():
|
|
891
|
-
click.echo("Update your Node")
|
|
892
222
|
try:
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
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
|
|
900
229
|
|
|
901
|
-
|
|
902
|
-
|
|
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.")
|
|
903
237
|
|
|
904
|
-
host = env_data.get("HOST", "")
|
|
905
|
-
audience = config_data.get("app_metadata", {}).get("audience", "")
|
|
906
|
-
descr = config_data.get("app_metadata", {}).get("name", "")
|
|
907
238
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
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("----------------------------")
|
|
916
252
|
|
|
917
|
-
asyncio.run(_async_update_node_at_start(env_data, config_data, audience, descr))
|
|
918
253
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
|
925
263
|
|
|
264
|
+
host = credentials['host']
|
|
265
|
+
private_key = credentials['private_key']
|
|
926
266
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
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
|
|
932
272
|
|
|
933
|
-
|
|
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())
|
|
934
277
|
|
|
935
|
-
|
|
936
|
-
with open("config.json", "r") as f:
|
|
937
|
-
config_file_content = f.read()
|
|
938
|
-
except Exception as e:
|
|
939
|
-
click.echo(f"Error reading config.json content: {e}")
|
|
278
|
+
if not signature_b64:
|
|
940
279
|
return
|
|
941
280
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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 = {
|
|
945
285
|
"host": host,
|
|
946
|
-
"
|
|
947
|
-
"
|
|
948
|
-
"node_type": audience,
|
|
949
|
-
"config_file": config_file_content,
|
|
950
|
-
"descr": descr,
|
|
286
|
+
"signed_message": signature_b64,
|
|
287
|
+
"message": message
|
|
951
288
|
}
|
|
952
289
|
|
|
953
|
-
async with aiohttp.ClientSession() as session:
|
|
954
|
-
try:
|
|
955
|
-
async with session.post(url, json=node) as response:
|
|
956
|
-
response.raise_for_status()
|
|
957
|
-
data = await response.json()
|
|
958
|
-
updated_node_id = data.get("nodeID", node_id)
|
|
959
|
-
click.echo(f"Neuronum Node '{updated_node_id}' updated!")
|
|
960
|
-
except aiohttp.ClientError as e:
|
|
961
|
-
click.echo(f"Error sending request: {e}")
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
@click.command()
|
|
965
|
-
def delete_node():
|
|
966
|
-
asyncio.run(async_delete_node())
|
|
967
|
-
|
|
968
|
-
async def async_delete_node():
|
|
969
|
-
credentials_folder_path = Path.home() / ".neuronum"
|
|
970
|
-
env_path = credentials_folder_path / ".env"
|
|
971
|
-
env_data = {}
|
|
972
|
-
|
|
973
290
|
try:
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
host = env_data.get("HOST", "")
|
|
980
|
-
password = env_data.get("PASSWORD", "")
|
|
981
|
-
network = env_data.get("NETWORK", "")
|
|
982
|
-
synapse = env_data.get("SYNAPSE", "")
|
|
983
|
-
|
|
984
|
-
with open('config.json', 'r') as f:
|
|
985
|
-
data = json.load(f)
|
|
986
|
-
|
|
987
|
-
nodeID = data['app_metadata']['node_id']
|
|
988
|
-
|
|
989
|
-
except FileNotFoundError:
|
|
990
|
-
click.echo("Error: .env with credentials not found")
|
|
991
|
-
return
|
|
992
|
-
except Exception as e:
|
|
993
|
-
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}")
|
|
994
296
|
return
|
|
995
297
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
"nodeID": nodeID,
|
|
999
|
-
"host": host,
|
|
1000
|
-
"password": password,
|
|
1001
|
-
"synapse": synapse
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
async with aiohttp.ClientSession() as session:
|
|
298
|
+
# 5. Cleanup Local Files
|
|
299
|
+
if status:
|
|
1005
300
|
try:
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
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.")
|
|
1015
310
|
|
|
1016
311
|
|
|
312
|
+
# --- CLI Registration ---
|
|
1017
313
|
cli.add_command(create_cell)
|
|
1018
|
-
cli.add_command(connect_cell)
|
|
1019
314
|
cli.add_command(view_cell)
|
|
1020
|
-
cli.add_command(
|
|
315
|
+
cli.add_command(connect_cell)
|
|
1021
316
|
cli.add_command(delete_cell)
|
|
1022
|
-
cli.add_command(init_node)
|
|
1023
|
-
cli.add_command(update_node)
|
|
1024
|
-
cli.add_command(start_node)
|
|
1025
|
-
cli.add_command(restart_node)
|
|
1026
|
-
cli.add_command(stop_node)
|
|
1027
|
-
cli.add_command(check_node)
|
|
1028
|
-
cli.add_command(delete_node)
|
|
1029
|
-
|
|
1030
317
|
|
|
1031
318
|
if __name__ == "__main__":
|
|
1032
319
|
cli()
|