hive-nectar 0.2.9__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.
Files changed (87) hide show
  1. hive_nectar-0.2.9.dist-info/METADATA +194 -0
  2. hive_nectar-0.2.9.dist-info/RECORD +87 -0
  3. hive_nectar-0.2.9.dist-info/WHEEL +4 -0
  4. hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
  5. hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
  6. nectar/__init__.py +37 -0
  7. nectar/account.py +5076 -0
  8. nectar/amount.py +553 -0
  9. nectar/asciichart.py +303 -0
  10. nectar/asset.py +122 -0
  11. nectar/block.py +574 -0
  12. nectar/blockchain.py +1242 -0
  13. nectar/blockchaininstance.py +2590 -0
  14. nectar/blockchainobject.py +263 -0
  15. nectar/cli.py +5937 -0
  16. nectar/comment.py +1552 -0
  17. nectar/community.py +854 -0
  18. nectar/constants.py +95 -0
  19. nectar/discussions.py +1437 -0
  20. nectar/exceptions.py +152 -0
  21. nectar/haf.py +381 -0
  22. nectar/hive.py +630 -0
  23. nectar/imageuploader.py +114 -0
  24. nectar/instance.py +113 -0
  25. nectar/market.py +876 -0
  26. nectar/memo.py +542 -0
  27. nectar/message.py +379 -0
  28. nectar/nodelist.py +309 -0
  29. nectar/price.py +603 -0
  30. nectar/profile.py +74 -0
  31. nectar/py.typed +0 -0
  32. nectar/rc.py +333 -0
  33. nectar/snapshot.py +1024 -0
  34. nectar/storage.py +62 -0
  35. nectar/transactionbuilder.py +659 -0
  36. nectar/utils.py +630 -0
  37. nectar/version.py +3 -0
  38. nectar/vote.py +722 -0
  39. nectar/wallet.py +472 -0
  40. nectar/witness.py +728 -0
  41. nectarapi/__init__.py +12 -0
  42. nectarapi/exceptions.py +126 -0
  43. nectarapi/graphenerpc.py +596 -0
  44. nectarapi/node.py +194 -0
  45. nectarapi/noderpc.py +79 -0
  46. nectarapi/openapi.py +107 -0
  47. nectarapi/py.typed +0 -0
  48. nectarapi/rpcutils.py +98 -0
  49. nectarapi/version.py +3 -0
  50. nectarbase/__init__.py +15 -0
  51. nectarbase/ledgertransactions.py +106 -0
  52. nectarbase/memo.py +242 -0
  53. nectarbase/objects.py +521 -0
  54. nectarbase/objecttypes.py +21 -0
  55. nectarbase/operationids.py +102 -0
  56. nectarbase/operations.py +1357 -0
  57. nectarbase/py.typed +0 -0
  58. nectarbase/signedtransactions.py +89 -0
  59. nectarbase/transactions.py +11 -0
  60. nectarbase/version.py +3 -0
  61. nectargraphenebase/__init__.py +27 -0
  62. nectargraphenebase/account.py +1121 -0
  63. nectargraphenebase/aes.py +49 -0
  64. nectargraphenebase/base58.py +197 -0
  65. nectargraphenebase/bip32.py +575 -0
  66. nectargraphenebase/bip38.py +110 -0
  67. nectargraphenebase/chains.py +15 -0
  68. nectargraphenebase/dictionary.py +2 -0
  69. nectargraphenebase/ecdsasig.py +309 -0
  70. nectargraphenebase/objects.py +130 -0
  71. nectargraphenebase/objecttypes.py +8 -0
  72. nectargraphenebase/operationids.py +5 -0
  73. nectargraphenebase/operations.py +25 -0
  74. nectargraphenebase/prefix.py +13 -0
  75. nectargraphenebase/py.typed +0 -0
  76. nectargraphenebase/signedtransactions.py +221 -0
  77. nectargraphenebase/types.py +557 -0
  78. nectargraphenebase/unsignedtransactions.py +288 -0
  79. nectargraphenebase/version.py +3 -0
  80. nectarstorage/__init__.py +57 -0
  81. nectarstorage/base.py +317 -0
  82. nectarstorage/exceptions.py +15 -0
  83. nectarstorage/interfaces.py +244 -0
  84. nectarstorage/masterpassword.py +237 -0
  85. nectarstorage/py.typed +0 -0
  86. nectarstorage/ram.py +27 -0
  87. nectarstorage/sqlite.py +343 -0
nectar/message.py ADDED
@@ -0,0 +1,379 @@
1
+ import json
2
+ import logging
3
+ import re
4
+ from binascii import hexlify, unhexlify
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Optional, Union
7
+
8
+ from nectar.account import Account
9
+ from nectar.instance import shared_blockchain_instance
10
+ from nectargraphenebase.account import PublicKey
11
+ from nectargraphenebase.ecdsasig import sign_message, verify_message
12
+
13
+ from .exceptions import (
14
+ AccountDoesNotExistsException,
15
+ InvalidMemoKeyException,
16
+ InvalidMessageSignature,
17
+ WrongMemoKey,
18
+ )
19
+
20
+ log = logging.getLogger(__name__)
21
+
22
+
23
+ class MessageV1:
24
+ """Allow to sign and verify Messages that are sigend with a private key"""
25
+
26
+ MESSAGE_SPLIT = (
27
+ "-----BEGIN HIVE SIGNED MESSAGE-----",
28
+ "-----BEGIN META-----",
29
+ "-----BEGIN SIGNATURE-----",
30
+ "-----END HIVE SIGNED MESSAGE-----",
31
+ )
32
+
33
+ # This is the message that is actually signed
34
+ SIGNED_MESSAGE_META = """{message}
35
+ account={meta[account]}
36
+ memokey={meta[memokey]}
37
+ block={meta[block]}
38
+ timestamp={meta[timestamp]}"""
39
+
40
+ SIGNED_MESSAGE_ENCAPSULATED = """
41
+ {MESSAGE_SPLIT[0]}
42
+ {message}
43
+ {MESSAGE_SPLIT[1]}
44
+ account={meta[account]}
45
+ memokey={meta[memokey]}
46
+ block={meta[block]}
47
+ timestamp={meta[timestamp]}
48
+ {MESSAGE_SPLIT[2]}
49
+ {signature}
50
+ {MESSAGE_SPLIT[3]}"""
51
+
52
+ def __init__(
53
+ self, message: str, blockchain_instance: Optional[Any] = None, *args: Any, **kwargs: Any
54
+ ) -> None:
55
+ """
56
+ Initialize the message handler, normalize line endings, and set up signing context.
57
+
58
+ Parameters:
59
+ message (str): The raw message text to be signed or verified. Line endings are normalized to LF.
60
+
61
+ Description:
62
+ - Assigns self.blockchain to the provided blockchain_instance or to shared_blockchain_instance() when none is given.
63
+ - Normalizes CRLF ("\r\n") to LF ("\n") and stores the result in self.message.
64
+ - Initializes signing/verification context attributes: signed_by_account, signed_by_name, meta, and plain_message to None.
65
+ """
66
+ self.blockchain = blockchain_instance or shared_blockchain_instance()
67
+ self.message = message.replace("\r\n", "\n")
68
+ self.signed_by_account = None
69
+ self.signed_by_name = None
70
+ self.meta = None
71
+ self.plain_message = None
72
+
73
+ def sign(self, account: Optional[Union[str, Account]] = None, **kwargs: Any) -> Any:
74
+ """Sign a message with an account's memo key
75
+ :param str account: (optional) the account that owns the bet
76
+ (defaults to ``default_account``)
77
+ :raises ValueError: If not account for signing is provided
78
+ :returns: the signed message encapsulated in a known format
79
+ """
80
+ if not account:
81
+ if "default_account" in self.blockchain.config:
82
+ account = self.blockchain.config["default_account"]
83
+ if not account:
84
+ raise ValueError("You need to provide an account")
85
+
86
+ # Data for message
87
+ account = Account(account, blockchain_instance=self.blockchain)
88
+ info = self.blockchain.info()
89
+ meta = dict(
90
+ timestamp=info["time"],
91
+ block=info["head_block_number"],
92
+ memokey=account["memo_key"],
93
+ account=account["name"],
94
+ )
95
+
96
+ # wif key
97
+ wif = self.blockchain.wallet.getPrivateKeyForPublicKey(account["memo_key"])
98
+
99
+ # We strip the message here so we know for sure there are no trailing
100
+ # whitespaces or returns
101
+ message = self.message.strip()
102
+
103
+ enc_message = self.SIGNED_MESSAGE_META.format(**locals())
104
+
105
+ # signature
106
+ signature = hexlify(sign_message(enc_message, wif)).decode("ascii")
107
+
108
+ self.signed_by_account = account
109
+ self.signed_by_name = account["name"]
110
+ self.meta = meta
111
+ self.plain_message = message
112
+
113
+ return self.SIGNED_MESSAGE_ENCAPSULATED.format(MESSAGE_SPLIT=self.MESSAGE_SPLIT, **locals())
114
+
115
+ def verify(self, **kwargs: Any) -> bool:
116
+ """Verify a message with an account's memo key
117
+ :param str account: (optional) the account that owns the bet
118
+ (defaults to ``default_account``)
119
+ :returns: True if the message is verified successfully
120
+ :raises InvalidMessageSignature if the signature is not ok
121
+ """
122
+ # Split message into its parts
123
+ parts = re.split("|".join(self.MESSAGE_SPLIT), self.message)
124
+ parts = [x for x in parts if x.strip()]
125
+
126
+ assert len(parts) > 2, "Incorrect number of message parts"
127
+
128
+ # Strip away all whitespaces before and after the message
129
+ message = parts[0].strip()
130
+ signature = parts[2].strip()
131
+ # Parse the meta data
132
+ meta = dict(re.findall(r"(\S+)=(.*)", parts[1]))
133
+
134
+ log.info(f"Message is: {message}")
135
+ log.info(f"Meta is: {json.dumps(meta)}")
136
+ log.info(f"Signature is: {signature}")
137
+
138
+ # Ensure we have all the data in meta
139
+ assert "account" in meta, "No 'account' could be found in meta data"
140
+ assert "memokey" in meta, "No 'memokey' could be found in meta data"
141
+ assert "block" in meta, "No 'block' could be found in meta data"
142
+ assert "timestamp" in meta, "No 'timestamp' could be found in meta data"
143
+
144
+ account_name = meta.get("account", "").strip()
145
+ memo_key = meta.get("memokey", "").strip()
146
+
147
+ try:
148
+ PublicKey(memo_key, prefix=self.blockchain.prefix)
149
+ except Exception:
150
+ raise InvalidMemoKeyException("The memo key in the message is invalid")
151
+
152
+ # Load account from blockchain
153
+ try:
154
+ account = Account(account_name, blockchain_instance=self.blockchain)
155
+ except AccountDoesNotExistsException:
156
+ raise AccountDoesNotExistsException(
157
+ "Could not find account {}. Are you connected to the right chain?".format(
158
+ account_name
159
+ )
160
+ )
161
+
162
+ # Test if memo key is the same as on the blockchain
163
+ if not account["memo_key"] == memo_key:
164
+ raise WrongMemoKey(
165
+ "Memo Key of account {} on the Blockchain ".format(account["name"])
166
+ + "differs from memo key in the message: {} != {}".format(
167
+ account["memo_key"], memo_key
168
+ )
169
+ )
170
+
171
+ # Reformat message
172
+ enc_message = self.SIGNED_MESSAGE_META.format(**locals())
173
+
174
+ # Verify Signature
175
+ pubkey = verify_message(enc_message, unhexlify(signature))
176
+ if pubkey is None:
177
+ raise InvalidMessageSignature("No public key recovered from signature")
178
+
179
+ # Verify pubky
180
+ pk = PublicKey(hexlify(pubkey).decode("ascii"), prefix=self.blockchain.prefix)
181
+ if format(pk, self.blockchain.prefix) != memo_key:
182
+ raise InvalidMessageSignature("The signature doesn't match the memo key")
183
+
184
+ self.signed_by_account = account
185
+ self.signed_by_name = account["name"]
186
+ self.meta = meta
187
+ self.plain_message = message
188
+
189
+ return True
190
+
191
+
192
+ class MessageV2:
193
+ """Allow to sign and verify Messages that are sigend with a private key"""
194
+
195
+ def __init__(
196
+ self, message: str, blockchain_instance: Optional[Any] = None, *args: Any, **kwargs: Any
197
+ ) -> None:
198
+ """
199
+ Initialize the message handler and set up default signing context.
200
+
201
+ Parameters:
202
+ message (str): The raw message text to be signed or verified.
203
+
204
+ Description:
205
+ Stores the provided message and sets up the signing context attributes
206
+ (signed_by_account, signed_by_name, meta, plain_message) to None.
207
+ If no blockchain instance is supplied, assigns a shared blockchain instance
208
+ via shared_blockchain_instance().
209
+ """
210
+ self.blockchain = blockchain_instance or shared_blockchain_instance()
211
+
212
+ self.message = message
213
+ self.signed_by_account = None
214
+ self.signed_by_name = None
215
+ self.meta = None
216
+ self.plain_message = None
217
+
218
+ def sign(self, account: Optional[Union[str, Account]] = None, **kwargs: Any) -> Any:
219
+ """Sign a message with an account's memo key
220
+ :param str account: (optional) the account that owns the bet
221
+ (defaults to ``default_account``)
222
+ :raises ValueError: If not account for signing is provided
223
+ :returns: the signed message encapsulated in a known format
224
+ """
225
+ if not account:
226
+ if "default_account" in self.blockchain.config:
227
+ account = self.blockchain.config["default_account"]
228
+ if not account:
229
+ raise ValueError("You need to provide an account")
230
+
231
+ # Data for message
232
+ account = Account(account, blockchain_instance=self.blockchain)
233
+
234
+ # wif key
235
+ wif = self.blockchain.wallet.getPrivateKeyForPublicKey(account["memo_key"])
236
+
237
+ payload = [
238
+ "from",
239
+ account["name"],
240
+ "key",
241
+ account["memo_key"],
242
+ "time",
243
+ str(datetime.now(timezone.utc)),
244
+ "text",
245
+ self.message,
246
+ ]
247
+ enc_message = json.dumps(payload, separators=(",", ":"))
248
+
249
+ # signature
250
+ signature = hexlify(sign_message(enc_message, wif)).decode("ascii")
251
+
252
+ return dict(signed=enc_message, payload=payload, signature=signature)
253
+
254
+ def verify(self, **kwargs: Any) -> bool:
255
+ """Verify a message with an account's memo key
256
+ :param str account: (optional) the account that owns the bet
257
+ (defaults to ``default_account``)
258
+ :returns: True if the message is verified successfully
259
+ :raises InvalidMessageSignature if the signature is not ok
260
+ """
261
+ if not isinstance(self.message, dict):
262
+ try:
263
+ self.message = json.loads(self.message)
264
+ except Exception:
265
+ raise ValueError("Message must be valid JSON")
266
+
267
+ payload = self.message.get("payload")
268
+ assert payload, "Missing payload"
269
+ payload_dict = {k[0]: k[1] for k in zip(payload[::2], payload[1::2])}
270
+ signature = self.message.get("signature")
271
+
272
+ account_name = payload_dict.get("from", "").strip()
273
+ memo_key = payload_dict.get("key", "").strip()
274
+
275
+ assert account_name, "Missing account name 'from'"
276
+ assert memo_key, "missing 'key'"
277
+
278
+ try:
279
+ # Validate that the memo key is a syntactically valid public key
280
+ PublicKey(memo_key, prefix=self.blockchain.prefix)
281
+ except Exception:
282
+ raise InvalidMemoKeyException("The memo key in the message is invalid")
283
+
284
+ # Load account from blockchain
285
+ try:
286
+ account = Account(account_name, blockchain_instance=self.blockchain)
287
+ except AccountDoesNotExistsException:
288
+ raise AccountDoesNotExistsException(
289
+ "Could not find account {}. Are you connected to the right chain?".format(
290
+ account_name
291
+ )
292
+ )
293
+
294
+ # Test if memo key is the same as on the blockchain
295
+ if not account["memo_key"] == memo_key:
296
+ raise WrongMemoKey(
297
+ "Memo Key of account {} on the Blockchain ".format(account["name"])
298
+ + "differs from memo key in the message: {} != {}".format(
299
+ account["memo_key"], memo_key
300
+ )
301
+ )
302
+
303
+ # Ensure payload and signed match
304
+ signed_target = json.dumps(self.message.get("payload"), separators=(",", ":"))
305
+ signed_actual = self.message.get("signed")
306
+ assert signed_target == signed_actual, (
307
+ "payload doesn't match signed message: \n{}\n{}".format(signed_target, signed_actual)
308
+ )
309
+
310
+ # Reformat message
311
+ enc_message = self.message.get("signed")
312
+
313
+ # Verify Signature
314
+ pubkey = verify_message(enc_message, unhexlify(signature))
315
+
316
+ # Verify pubky
317
+ if pubkey is None:
318
+ raise InvalidMessageSignature("No public key recovered from signature")
319
+ pk = PublicKey(hexlify(pubkey).decode("ascii"), prefix=self.blockchain.prefix)
320
+ if format(pk, self.blockchain.prefix) != memo_key:
321
+ raise InvalidMessageSignature("The signature doesn't match the memo key")
322
+
323
+ self.signed_by_account = account
324
+ self.signed_by_name = account["name"]
325
+ self.plain_message = payload_dict.get("text")
326
+
327
+ return True
328
+
329
+
330
+ class Message(MessageV1, MessageV2):
331
+ supported_formats = (MessageV1, MessageV2)
332
+ valid_exceptions = (
333
+ AccountDoesNotExistsException,
334
+ InvalidMessageSignature,
335
+ WrongMemoKey,
336
+ InvalidMemoKeyException,
337
+ )
338
+
339
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
340
+ for _format in self.supported_formats:
341
+ try:
342
+ _format.__init__(self, *args, **kwargs)
343
+ return
344
+ except self.valid_exceptions as e:
345
+ raise e
346
+ except Exception as e:
347
+ log.warning(
348
+ "{}: Couldn't init: {}: {}".format(
349
+ _format.__name__, e.__class__.__name__, str(e)
350
+ )
351
+ )
352
+
353
+ def verify(self, **kwargs: Any) -> bool:
354
+ for _format in self.supported_formats:
355
+ try:
356
+ return _format.verify(self, **kwargs)
357
+ except self.valid_exceptions as e:
358
+ raise e
359
+ except Exception as e:
360
+ log.warning(
361
+ "{}: Couldn't verify: {}: {}".format(
362
+ _format.__name__, e.__class__.__name__, str(e)
363
+ )
364
+ )
365
+ raise ValueError("No Decoder accepted the message")
366
+
367
+ def sign(self, account: Optional[Union[str, Account]] = None, **kwargs: Any) -> Any:
368
+ for _format in self.supported_formats:
369
+ try:
370
+ return _format.sign(self, account=account, **kwargs)
371
+ except self.valid_exceptions as e:
372
+ raise e
373
+ except Exception as e:
374
+ log.warning(
375
+ "{}: Couldn't sign: {}: {}".format(
376
+ _format.__name__, e.__class__.__name__, str(e)
377
+ )
378
+ )
379
+ raise ValueError("No Decoder accepted the message")
nectar/nodelist.py ADDED
@@ -0,0 +1,309 @@
1
+ import json
2
+ import logging
3
+ import threading
4
+ import time
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import httpx
8
+
9
+ log = logging.getLogger(__name__)
10
+
11
+ # Static fallback nodes in case beacon is unavailable
12
+ STATIC_NODES = [
13
+ "https://api.hive.blog",
14
+ "https://api.openhive.network",
15
+ "https://api.syncad.com",
16
+ "https://api.deathwing.me",
17
+ "https://api.c0ff33a.uk",
18
+ "https://hive-api.3speak.tv",
19
+ "https://hiveapi.actifit.io",
20
+ "https://rpc.mahdiyari.info",
21
+ "https://techcoderx.com",
22
+ "https://anyx.io",
23
+ ]
24
+
25
+ BEACON_URL = "https://beacon.peakd.com/api/nodes"
26
+ REQUEST_TIMEOUT = 10 # seconds
27
+ CACHE_DURATION = 300 # 5 minutes cache
28
+
29
+ # Global cache for node data
30
+ _cached_nodes: Optional[List[Dict[str, Any]]] = None
31
+ _cache_timestamp: float = 0
32
+ _cache_lock = threading.Lock()
33
+
34
+
35
+ def fetch_beacon_nodes() -> Optional[List[Dict[str, Any]]]:
36
+ """Fetch node list from PeakD beacon API with caching.
37
+
38
+ Returns:
39
+ List of node dictionaries from beacon API, or None if fetch fails
40
+ """
41
+ global _cached_nodes, _cache_timestamp
42
+
43
+ current_time = time.time()
44
+
45
+ # Return cached data if still valid
46
+ with _cache_lock:
47
+ if _cached_nodes is not None and current_time - _cache_timestamp < CACHE_DURATION:
48
+ log.debug("Using cached beacon nodes")
49
+ return _cached_nodes
50
+
51
+ try:
52
+ log.debug("Fetching fresh nodes from beacon API")
53
+ with httpx.Client(timeout=REQUEST_TIMEOUT) as client:
54
+ response = client.get(BEACON_URL, headers={"Accept": "*/*"})
55
+ response.raise_for_status()
56
+ nodes = response.json()
57
+
58
+ # Cache the successful result
59
+ with _cache_lock:
60
+ _cached_nodes = nodes
61
+ _cache_timestamp = current_time
62
+ return nodes
63
+ except (httpx.RequestError, httpx.HTTPStatusError, json.JSONDecodeError, Exception) as e:
64
+ log.warning(f"Failed to fetch nodes from beacon API: {e}")
65
+ # Return cached data even if expired, as fallback
66
+ with _cache_lock:
67
+ if _cached_nodes is not None:
68
+ log.info("Using expired cached nodes as fallback")
69
+ return _cached_nodes
70
+ return None
71
+
72
+
73
+ def clear_beacon_cache() -> None:
74
+ """Clear the cached beacon node data.
75
+
76
+ This forces the next NodeList() instantiation or update_nodes() call
77
+ to fetch fresh data from the beacon API.
78
+ """
79
+ global _cached_nodes, _cache_timestamp
80
+ with _cache_lock:
81
+ _cached_nodes = None
82
+ _cache_timestamp = 0
83
+ log.debug("Beacon node cache cleared")
84
+
85
+
86
+ class NodeList(list):
87
+ """Simplified Hive node list using PeakD beacon API.
88
+
89
+ Fetches real-time node information from PeakD beacon API with static fallback.
90
+
91
+ .. code-block:: python
92
+
93
+ from nectar.nodelist import NodeList
94
+ n = NodeList()
95
+ nodes_urls = n.get_nodes()
96
+ """
97
+
98
+ def __init__(self):
99
+ """Initialize NodeList with nodes from beacon API or static fallback."""
100
+ super().__init__()
101
+ self._refresh_nodes()
102
+
103
+ def _refresh_nodes(self) -> None:
104
+ """Refresh node list from beacon API or use static fallback."""
105
+ beacon_nodes = fetch_beacon_nodes()
106
+
107
+ if beacon_nodes:
108
+ # Convert beacon format to our internal format
109
+ nodes = []
110
+ for node in beacon_nodes:
111
+ # Only include nodes with decent performance (score > 0)
112
+ if node.get("score", 0) > 0:
113
+ nodes.append(
114
+ {
115
+ "url": node["endpoint"],
116
+ "version": node.get("version", "unknown"),
117
+ "type": "appbase", # All beacon nodes are appbase
118
+ "owner": node.get("name", "unknown"),
119
+ "hive": True,
120
+ "score": node.get("score", 0),
121
+ }
122
+ )
123
+
124
+ # Sort by score (highest first)
125
+ nodes.sort(key=lambda x: x["score"], reverse=True)
126
+ super().__init__(nodes)
127
+ log.info(f"Loaded {len(nodes)} nodes from PeakD beacon API")
128
+ else:
129
+ # Use static fallback
130
+ nodes = [
131
+ {
132
+ "url": url,
133
+ "version": "unknown",
134
+ "type": "appbase",
135
+ "owner": "static",
136
+ "hive": True,
137
+ "score": 50,
138
+ }
139
+ for url in STATIC_NODES
140
+ ]
141
+ super().__init__(nodes)
142
+ log.warning(f"Using static fallback nodes ({len(nodes)} nodes)")
143
+
144
+ def get_nodes(
145
+ self,
146
+ hive: bool = True,
147
+ dev: bool = False,
148
+ testnet: bool = False,
149
+ testnetdev: bool = False,
150
+ wss: bool = True,
151
+ https: bool = True,
152
+ not_working: bool = False,
153
+ normal: bool = False,
154
+ appbase: bool = True,
155
+ ) -> List[str]:
156
+ """Return a list of node URLs filtered and sorted by score.
157
+
158
+ Args:
159
+ hive: Filter for Hive nodes only (default: True)
160
+ dev: Include dev nodes (not applicable with beacon)
161
+ testnet: Include testnet nodes (not applicable with beacon)
162
+ testnetdev: Include testnet dev nodes (not applicable with beacon)
163
+ wss: Include WebSocket nodes (default: True)
164
+ https: Include HTTPS nodes (default: True)
165
+ not_working: Include nodes with negative scores (default: False)
166
+ normal: Include normal nodes (deprecated, default: False)
167
+ appbase: Include appbase nodes (default: True)
168
+
169
+ Returns:
170
+ List of node URLs sorted by score (highest first)
171
+ """
172
+ filtered_nodes = []
173
+
174
+ # Determine allowed types based on flags (OR logic)
175
+ allowed_types = set()
176
+ if appbase:
177
+ allowed_types.add("appbase")
178
+ if normal:
179
+ allowed_types.add("normal")
180
+ if dev:
181
+ allowed_types.add("appbase-dev")
182
+ if testnet:
183
+ allowed_types.add("testnet")
184
+ if testnetdev:
185
+ allowed_types.add("testnet-dev")
186
+
187
+ for node in self:
188
+ # Filter by score
189
+ if node["score"] < 0 and not not_working:
190
+ continue
191
+
192
+ # Filter by Hive
193
+ if hive and not node["hive"]:
194
+ continue
195
+
196
+ # Filter by protocol
197
+ if not https and node["url"].startswith("https"):
198
+ continue
199
+ if not wss and node["url"].startswith("wss"):
200
+ continue
201
+
202
+ # Filter by type (OR logic)
203
+ if allowed_types and node["type"] not in allowed_types:
204
+ continue
205
+
206
+ filtered_nodes.append(node)
207
+
208
+ # Sort by score (highest first) and return URLs
209
+ filtered_nodes.sort(key=lambda x: x["score"], reverse=True)
210
+ return [node["url"] for node in filtered_nodes]
211
+
212
+ def get_hive_nodes(
213
+ self, testnet: bool = False, not_working: bool = False, wss: bool = True, https: bool = True
214
+ ) -> List[str]:
215
+ """Return a list of Hive node URLs filtered and ordered by score.
216
+
217
+ Args:
218
+ testnet: Include testnet nodes (default: False)
219
+ not_working: Include nodes with negative scores (default: False)
220
+ wss: Include WebSocket nodes (default: True)
221
+ https: Include HTTPS nodes (default: True)
222
+
223
+ Returns:
224
+ List of Hive node URLs sorted by score
225
+ """
226
+ return self.get_nodes(
227
+ hive=True,
228
+ testnet=testnet,
229
+ not_working=not_working,
230
+ wss=wss,
231
+ https=https,
232
+ appbase=True,
233
+ normal=False,
234
+ )
235
+
236
+ def get_testnet(self, testnet: bool = True, testnetdev: bool = False) -> List[str]:
237
+ """Return a list of testnet node URLs (currently unavailable).
238
+
239
+ Note: The PeakD beacon API does not provide testnet nodes. This method
240
+ currently returns an empty list. Use mainnet nodes for testing or
241
+ manually configure testnet endpoints.
242
+
243
+ Args:
244
+ testnet: Include testnet nodes (default: True)
245
+ testnetdev: Include testnet dev nodes (default: False)
246
+
247
+ Returns:
248
+ List of testnet node URLs
249
+ """
250
+ log.warning("Testnet nodes are not available from beacon API")
251
+ return self.get_nodes(
252
+ normal=False,
253
+ appbase=False,
254
+ testnet=testnet,
255
+ testnetdev=testnetdev,
256
+ )
257
+
258
+ def update_nodes(self, weights: Any = None, blockchain_instance: Any = None) -> None:
259
+ """Refresh nodes from beacon API.
260
+
261
+ This method replaces the complex update logic with a simple refresh
262
+ from the beacon API and clears the cache to force fresh data.
263
+
264
+ Args:
265
+ weights: Ignored (beacon provides its own scoring)
266
+ blockchain_instance: Ignored (beacon API is independent)
267
+ """
268
+ clear_beacon_cache()
269
+ self._refresh_nodes()
270
+
271
+ def update(self, node_list: List[str]) -> None:
272
+ """Update node list (not implemented with beacon API).
273
+
274
+ Args:
275
+ node_list: List of node URLs (ignored with beacon API)
276
+ """
277
+ log.info("NodeList.update() is deprecated with beacon API - using beacon data instead")
278
+ self._refresh_nodes()
279
+
280
+ def get_node_answer_time(
281
+ self, node_list: Optional[List[str]] = None, verbose: bool = False
282
+ ) -> List[Dict[str, float]]:
283
+ """Get node response times (deprecated with beacon API).
284
+
285
+ The beacon API already provides performance scoring, so this method
286
+ returns the beacon scores instead of measuring response times.
287
+
288
+ Args:
289
+ node_list: List of node URLs to check (ignored, uses all nodes)
290
+ verbose: Log node information (default: False)
291
+
292
+ Returns:
293
+ List of dictionaries with 'url' and 'delay_ms' keys
294
+ """
295
+ log.info("get_node_answer_time() using beacon scores instead of measuring response times")
296
+
297
+ result = []
298
+ for node in self:
299
+ # Convert beacon score (0-100) to a fake delay for compatibility
300
+ # Higher score = lower delay
301
+ fake_delay_ms = (100 - node["score"]) * 10 # Simple conversion
302
+ result.append({"url": node["url"], "delay_ms": fake_delay_ms})
303
+
304
+ if verbose:
305
+ log.info(
306
+ f"node {node['url']} beacon score: {node['score']}, fake delay: {fake_delay_ms:.2f}ms"
307
+ )
308
+
309
+ return sorted(result, key=lambda x: x["delay_ms"])