hive-nectar 0.0.11__py3-none-any.whl → 0.1.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 hive-nectar might be problematic. Click here for more details.

Files changed (56) hide show
  1. {hive_nectar-0.0.11.dist-info → hive_nectar-0.1.0.dist-info}/METADATA +10 -11
  2. hive_nectar-0.1.0.dist-info/RECORD +88 -0
  3. nectar/__init__.py +1 -4
  4. nectar/account.py +791 -685
  5. nectar/amount.py +82 -21
  6. nectar/asset.py +1 -2
  7. nectar/block.py +34 -22
  8. nectar/blockchain.py +111 -143
  9. nectar/blockchaininstance.py +396 -247
  10. nectar/blockchainobject.py +33 -5
  11. nectar/cli.py +1058 -1349
  12. nectar/comment.py +313 -181
  13. nectar/community.py +39 -43
  14. nectar/constants.py +1 -14
  15. nectar/discussions.py +793 -139
  16. nectar/hive.py +137 -77
  17. nectar/hivesigner.py +106 -68
  18. nectar/imageuploader.py +33 -23
  19. nectar/instance.py +31 -79
  20. nectar/market.py +128 -264
  21. nectar/memo.py +40 -13
  22. nectar/message.py +23 -10
  23. nectar/nodelist.py +115 -81
  24. nectar/price.py +80 -61
  25. nectar/profile.py +6 -3
  26. nectar/rc.py +45 -25
  27. nectar/snapshot.py +285 -163
  28. nectar/storage.py +16 -5
  29. nectar/transactionbuilder.py +132 -41
  30. nectar/utils.py +37 -17
  31. nectar/version.py +1 -1
  32. nectar/vote.py +171 -30
  33. nectar/wallet.py +26 -19
  34. nectar/witness.py +153 -54
  35. nectarapi/graphenerpc.py +147 -133
  36. nectarapi/noderpc.py +12 -6
  37. nectarapi/rpcutils.py +12 -6
  38. nectarapi/version.py +1 -1
  39. nectarbase/ledgertransactions.py +24 -1
  40. nectarbase/objects.py +17 -6
  41. nectarbase/operations.py +160 -90
  42. nectarbase/signedtransactions.py +38 -2
  43. nectarbase/version.py +1 -1
  44. nectargraphenebase/account.py +295 -17
  45. nectargraphenebase/chains.py +0 -135
  46. nectargraphenebase/ecdsasig.py +152 -176
  47. nectargraphenebase/types.py +18 -4
  48. nectargraphenebase/unsignedtransactions.py +1 -1
  49. nectargraphenebase/version.py +1 -1
  50. hive_nectar-0.0.11.dist-info/RECORD +0 -91
  51. nectar/blurt.py +0 -562
  52. nectar/conveyor.py +0 -308
  53. nectar/steem.py +0 -581
  54. {hive_nectar-0.0.11.dist-info → hive_nectar-0.1.0.dist-info}/WHEEL +0 -0
  55. {hive_nectar-0.0.11.dist-info → hive_nectar-0.1.0.dist-info}/entry_points.txt +0 -0
  56. {hive_nectar-0.0.11.dist-info → hive_nectar-0.1.0.dist-info}/licenses/LICENSE.txt +0 -0
nectarapi/graphenerpc.py CHANGED
@@ -2,7 +2,9 @@
2
2
  import json
3
3
  import logging
4
4
  import re
5
- import ssl
5
+
6
+ import requests
7
+ from requests.exceptions import ConnectionError
6
8
 
7
9
  from nectargraphenebase.chains import known_chains
8
10
  from nectargraphenebase.version import version as nectar_version
@@ -18,30 +20,6 @@ from .exceptions import (
18
20
  from .node import Nodes
19
21
  from .rpcutils import get_api_name, get_query, is_network_appbase_ready
20
22
 
21
- WEBSOCKET_MODULE = None
22
- if not WEBSOCKET_MODULE:
23
- try:
24
- import websocket
25
- from websocket._exceptions import (
26
- WebSocketConnectionClosedException,
27
- WebSocketTimeoutException,
28
- )
29
-
30
- WEBSOCKET_MODULE = "websocket"
31
- except ImportError:
32
- WEBSOCKET_MODULE = None
33
- REQUEST_MODULE = None
34
- if not REQUEST_MODULE:
35
- try:
36
- import requests
37
- from requests.adapters import HTTPAdapter
38
- from requests.exceptions import ConnectionError
39
- from requests.packages.urllib3.util.retry import Retry
40
-
41
- REQUEST_MODULE = "requests"
42
- except ImportError:
43
- REQUEST_MODULE = None
44
-
45
23
  log = logging.getLogger(__name__)
46
24
 
47
25
 
@@ -57,33 +35,27 @@ def set_session_instance(instance):
57
35
 
58
36
 
59
37
  def shared_session_instance():
60
- """Get session instance"""
61
- if REQUEST_MODULE is None:
62
- raise Exception("Requests module is not available.")
38
+ """
39
+ Return a singleton requests.Session instance, creating it if necessary.
40
+
41
+ Ensures a single shared HTTP session is reused across the process to take advantage
42
+ of connection pooling and shared session state (headers, cookies, adapters).
43
+
44
+ Returns:
45
+ requests.Session: The shared session object.
46
+ """
63
47
  if not SessionInstance.instance:
64
48
  SessionInstance.instance = requests.Session()
65
49
  return SessionInstance.instance
66
50
 
67
51
 
68
- def create_ws_instance(use_ssl=True, enable_multithread=True):
69
- """Get websocket instance"""
70
- if WEBSOCKET_MODULE is None:
71
- raise Exception("WebSocket module is not available.")
72
- if use_ssl:
73
- ssl_defaults = ssl.get_default_verify_paths()
74
- sslopt_ca_certs = {"ca_certs": ssl_defaults.cafile}
75
- return websocket.WebSocket(sslopt=sslopt_ca_certs, enable_multithread=enable_multithread)
76
- else:
77
- return websocket.WebSocket(enable_multithread=enable_multithread)
78
-
79
-
80
52
  class GrapheneRPC(object):
81
53
  """
82
54
  This class allows calling API methods synchronously, without callbacks.
83
55
 
84
56
  It logs warnings and errors.
85
57
 
86
- :param str urls: Either a single Websocket/Http URL, or a list of URLs
58
+ :param str urls: Either a single HTTP URL, or a list of HTTP URLs
87
59
  :param str user: Username for Authentication
88
60
  :param str password: Password for Authentication
89
61
  :param int num_retries: Number of retries for node connection (default is 100)
@@ -96,9 +68,27 @@ class GrapheneRPC(object):
96
68
  """
97
69
 
98
70
  def __init__(self, urls, user=None, password=None, **kwargs):
99
- """Initialize the RPC client."""
100
- self.rpc_methods = {"offline": -1, "ws": 0, "jsonrpc": 1, "wsappbase": 2, "appbase": 3}
101
- self.current_rpc = self.rpc_methods["ws"]
71
+ """
72
+ Create a synchronous HTTP RPC client for Graphene-based nodes.
73
+
74
+ Initializes RPC mode, retry/timeouts, node management, optional credentials, and feature flags. Supported keyword arguments (with defaults) control behavior:
75
+ - timeout (int): request timeout in seconds (default 60).
76
+ - num_retries (int): number of node-retry attempts for node selection (default 100).
77
+ - num_retries_call (int): per-call retry attempts before switching nodes (default 5).
78
+ - use_condenser (bool): prefer condenser API compatibility (default False).
79
+ - use_tor (bool): enable Tor proxies for the shared HTTP session (default False).
80
+ - disable_chain_detection (bool): skip automatic chain/appbase detection (default False).
81
+ - custom_chains (dict): mapping of additional known chain configurations to merge into the client's known_chains.
82
+ - autoconnect (bool): if True (default), attempts to connect to a working node immediately via rpcconnect().
83
+
84
+ Credentials:
85
+ - user, password: optional basic-auth credentials applied to HTTP requests.
86
+
87
+ Side effects:
88
+ - Builds a Nodes instance for node tracking and may call rpcconnect() when autoconnect is True.
89
+ """
90
+ self.rpc_methods = {"offline": -1, "appbase": 3}
91
+ self.current_rpc = self.rpc_methods["appbase"]
102
92
  self._request_id = 0
103
93
  self.timeout = kwargs.get("timeout", 60)
104
94
  num_retries = kwargs.get("num_retries", 100)
@@ -119,7 +109,6 @@ class GrapheneRPC(object):
119
109
 
120
110
  self.user = user
121
111
  self.password = password
122
- self.ws = None
123
112
  self.url = None
124
113
  self.session = None
125
114
  self.rpc_queue = []
@@ -148,24 +137,37 @@ class GrapheneRPC(object):
148
137
  return self._request_id
149
138
 
150
139
  def next(self):
151
- """Switches to the next node url"""
152
- if self.ws:
153
- try:
154
- self.rpcclose()
155
- except Exception as e:
156
- log.warning(str(e))
140
+ """
141
+ Advance to the next available RPC node and attempt to (re)connect.
142
+ """
157
143
  self.rpcconnect()
158
144
 
159
145
  def is_appbase_ready(self):
160
146
  """Check if node is appbase ready"""
161
- return self.current_rpc in [self.rpc_methods["wsappbase"], self.rpc_methods["appbase"]]
147
+ return self.current_rpc == self.rpc_methods["appbase"]
162
148
 
163
149
  def get_use_appbase(self):
164
- """Returns True if appbase ready and appbase calls are set"""
150
+ """
151
+ Return True if AppBase RPC calls should be used.
152
+
153
+ Returns:
154
+ bool: True when AppBase is ready (is_appbase_ready()) and the instance is not configured to use the condenser API (use_condenser is False).
155
+ """
165
156
  return not self.use_condenser and self.is_appbase_ready()
166
157
 
167
158
  def rpcconnect(self, next_url=True):
168
- """Connect to next url in a loop."""
159
+ """
160
+ Selects and establishes connection to an available RPC node.
161
+
162
+ Attempts to connect to the next available node (or reuse the current one) and initializes per-instance HTTP session state needed for subsequent RPC calls. On a successful connection this method sets: self.url, self.session (shared session reused), self._proxies (Tor proxies when configured), self.headers, and self.current_rpc (appbase mode by default). It also probes the node using get_config to detect whether the node supports appbase RPC format unless chain detection is disabled.
163
+
164
+ Parameters:
165
+ next_url (bool): If True, advance to the next node before attempting connection; if False, retry the current node.
166
+
167
+ Raises:
168
+ RPCError: When a get_config probe returns no properties (connection reached but no config received).
169
+ KeyboardInterrupt: Propagated if the operation is interrupted by the user.
170
+ """
169
171
  if self.nodes.working_nodes_count == 0:
170
172
  return
171
173
  while True:
@@ -173,36 +175,26 @@ class GrapheneRPC(object):
173
175
  self.url = next(self.nodes)
174
176
  self.nodes.reset_error_cnt_call()
175
177
  log.debug("Trying to connect to node %s" % self.url)
176
- if self.url[:3] == "wss":
177
- self.ws = create_ws_instance(use_ssl=True)
178
- self.ws.settimeout(self.timeout)
179
- self.current_rpc = self.rpc_methods["wsappbase"]
180
- elif self.url[:2] == "ws":
181
- self.ws = create_ws_instance(use_ssl=False)
182
- self.ws.settimeout(self.timeout)
183
- self.current_rpc = self.rpc_methods["wsappbase"]
184
- else:
185
- self.ws = None
186
- self.session = shared_session_instance()
187
- if self.use_tor:
188
- self.session.proxies = {}
189
- self.session.proxies["http"] = "socks5h://localhost:9050"
190
- self.session.proxies["https"] = "socks5h://localhost:9050"
191
- self.current_rpc = self.rpc_methods["appbase"]
192
- self.headers = {
193
- "User-Agent": "nectar v%s" % (nectar_version),
194
- "content-type": "application/json; charset=utf-8",
178
+ self.ws = None
179
+ self.session = shared_session_instance()
180
+ self.ws = None
181
+ self.session = shared_session_instance()
182
+ # Do not mutate the shared session; store per-instance proxies.
183
+ self._proxies = None
184
+ if self.use_tor:
185
+ self._proxies = {
186
+ "http": "socks5h://localhost:9050",
187
+ "https": "socks5h://localhost:9050",
195
188
  }
189
+ self.current_rpc = self.rpc_methods["appbase"]
190
+ self.headers = {
191
+ "User-Agent": "nectar v%s" % (nectar_version),
192
+ "content-type": "application/json; charset=utf-8",
193
+ }
196
194
  try:
197
- if self.ws:
198
- self.ws.connect(self.url)
199
- self.rpclogin(self.user, self.password)
200
195
  if self.disable_chain_detection:
201
196
  # Set to appbase rpc format
202
- if self.current_rpc == self.rpc_methods["ws"]:
203
- self.current_rpc = self.rpc_methods["wsappbase"]
204
- else:
205
- self.current_rpc = self.rpc_methods["appbase"]
197
+ self.current_rpc = self.rpc_methods["appbase"]
206
198
  break
207
199
  try:
208
200
  props = None
@@ -213,18 +205,12 @@ class GrapheneRPC(object):
213
205
  except Exception as e:
214
206
  if re.search("Bad Cast:Invalid cast from type", str(e)):
215
207
  # retry with not appbase
216
- if self.current_rpc == self.rpc_methods["wsappbase"]:
217
- self.current_rpc = self.rpc_methods["ws"]
218
- else:
219
- self.current_rpc = self.rpc_methods["appbase"]
208
+ self.current_rpc = self.rpc_methods["appbase"]
220
209
  props = self.get_config(api="database")
221
210
  if props is None:
222
211
  raise RPCError("Could not receive answer for get_config")
223
212
  if is_network_appbase_ready(props):
224
- if self.ws:
225
- self.current_rpc = self.rpc_methods["wsappbase"]
226
- else:
227
- self.current_rpc = self.rpc_methods["appbase"]
213
+ self.current_rpc = self.rpc_methods["appbase"]
228
214
  break
229
215
  except KeyboardInterrupt:
230
216
  raise
@@ -234,19 +220,21 @@ class GrapheneRPC(object):
234
220
  self.nodes.sleep_and_check_retries(str(e), sleep=do_sleep)
235
221
  next_url = True
236
222
 
237
- def rpclogin(self, user, password):
238
- """Login into Websocket"""
239
- if self.ws and self.current_rpc == self.rpc_methods["ws"] and user and password:
240
- self.login(user, password, api="login_api")
223
+ def request_send(self, payload):
224
+ """
225
+ Send the prepared RPC payload to the currently connected node via HTTP POST.
241
226
 
242
- def rpcclose(self):
243
- """Close Websocket"""
244
- if self.ws is None:
245
- return
246
- # if self.ws.connected:
247
- self.ws.close()
227
+ Sends `payload` to the client's active URL using the shared HTTP session. If username and password were provided to the client, HTTP basic auth is applied. Raises UnauthorizedError when the node responds with HTTP 401.
248
228
 
249
- def request_send(self, payload):
229
+ Parameters:
230
+ payload (str | bytes): The JSON-RPC payload (string or bytes) to send in the POST body.
231
+
232
+ Returns:
233
+ requests.Response: The raw HTTP response object from the node.
234
+
235
+ Raises:
236
+ UnauthorizedError: If the HTTP response status code is 401 (Unauthorized).
237
+ """
250
238
  if self.user is not None and self.password is not None:
251
239
  response = self.session.post(
252
240
  self.url,
@@ -263,20 +251,49 @@ class GrapheneRPC(object):
263
251
  raise UnauthorizedError
264
252
  return response
265
253
 
266
- def ws_send(self, payload):
267
- if self.ws is None:
268
- raise RPCConnection("No websocket available!")
269
- self.ws.send(payload)
270
- reply = self.ws.recv()
271
- return reply
272
-
273
254
  def version_string_to_int(self, network_version):
255
+ """
256
+ Convert a dotted version string "MAJOR.MINOR.PATCH" into a single integer for easy comparison.
257
+
258
+ The integer is computed as: major * 10^8 + minor * 10^4 + patch. For example, "2.3.15" -> 200030015.
259
+
260
+ Parameters:
261
+ network_version (str): Version string in the form "major.minor.patch".
262
+
263
+ Returns:
264
+ int: Integer representation suitable for numeric comparisons.
265
+
266
+ Raises:
267
+ ValueError: If any version component is not an integer.
268
+ IndexError: If the version string does not contain three components.
269
+ """
274
270
  version_list = network_version.split(".")
275
271
  return int(int(version_list[0]) * 1e8 + int(version_list[1]) * 1e4 + int(version_list[2]))
276
272
 
277
273
  def get_network(self, props=None):
278
- """Identify the connected network. This call returns a
279
- dictionary with keys chain_id, core_symbol and prefix
274
+ """
275
+ Detects and returns the network/chain configuration for the connected node.
276
+
277
+ If props is not provided, this call fetches node configuration via get_config(api="database") and inspects property keys to determine the chain identifier, address prefix, network/version, and core asset definitions. It builds a chain configuration dict with keys:
278
+ - chain_id: canonical chain identifier string
279
+ - prefix: account/address prefix for the network
280
+ - min_version: reported chain version string
281
+ - chain_assets: list of asset dicts (each with keys "asset" (NAI), "precision", "symbol", and "id")
282
+
283
+ If the detected chain matches an entry in self.known_chains (preferring the highest compatible known min_version), that known_chains entry is returned instead of the freshly built config.
284
+
285
+ Special behaviors:
286
+ - When props is None, get_config(api="database") is called.
287
+ - If detection finds conflicting blockchain prefixes, the most frequent prefix is used.
288
+ - A legacy fallback removes STEEM_CHAIN_ID from props if no blockchain name is inferred, logging a warning to prefer HIVE.
289
+ - Test-network asset NAIs are mapped to "TBD" or "TESTS" symbols when appropriate.
290
+ - Asset entries are assigned stable incremental ids based on sorted NAI order.
291
+
292
+ Returns:
293
+ dict: A chain configuration (either a matching entry from self.known_chains or a freshly constructed chain_config) with keys described above.
294
+
295
+ Raises:
296
+ RPCError: If chain_id cannot be determined or no compatible known chain is found.
280
297
  """
281
298
  if props is None:
282
299
  props = self.get_config(api="database")
@@ -298,8 +315,12 @@ class GrapheneRPC(object):
298
315
  sorted_prefix_count = sorted(prefix_count.items(), key=lambda x: x[1], reverse=True)
299
316
  if sorted_prefix_count[0][1] > 1:
300
317
  blockchain_name = sorted_prefix_count[0][0]
301
- if blockchain_name is None and "HIVE_CHAIN_ID" in props and "STEEM_CHAIN_ID" in props:
302
- del props["STEEM_CHAIN_ID"]
318
+
319
+ # Check for configurable chain preference
320
+ if blockchain_name is None:
321
+ if "STEEM_CHAIN_ID" in props:
322
+ del props["STEEM_CHAIN_ID"]
323
+ log.warning("Using fallback chain preference: HIVE (STEEM removed from detection)")
303
324
 
304
325
  for key in props:
305
326
  if key[-8:] == "CHAIN_ID" and blockchain_name is None:
@@ -364,7 +385,6 @@ class GrapheneRPC(object):
364
385
  if (
365
386
  blockchain_name is not None
366
387
  and blockchain_name not in k
367
- and blockchain_name != "STEEMIT"
368
388
  and blockchain_name != "CHAIN"
369
389
  ):
370
390
  continue
@@ -425,11 +445,21 @@ class GrapheneRPC(object):
425
445
 
426
446
  def rpcexec(self, payload):
427
447
  """
428
- Execute a call by sending the payload.
448
+ Execute the given JSON-RPC payload against the currently selected node and return the RPC result.
449
+
450
+ Sends an HTTP POST with `payload` to the connected node, handling empty responses, retries, node rotation, and JSON parsing. On success returns either the `result` field for single-response RPC calls or a list of results when the server returns a JSON-RPC batch/array. Resets per-call error counters on successful responses.
429
451
 
430
- :param json payload: Payload data
431
- :raises ValueError: if the server does not respond in proper JSON format
432
- :raises RPCError: if the server returns an error
452
+ Parameters:
453
+ payload (dict or list): JSON-serializable RPC request object or a list of request objects (batch).
454
+
455
+ Returns:
456
+ The RPC `result` (any) for a single request, or a list of results for a batch response.
457
+
458
+ Raises:
459
+ WorkingNodeMissing: if no working nodes are available.
460
+ RPCConnection: if the client is not connected to any node.
461
+ RPCError: for server-reported errors or unexpected / non-JSON responses that indicate an RPC failure.
462
+ KeyboardInterrupt: if execution is interrupted by the user.
433
463
  """
434
464
  log.debug(f"Payload: {json.dumps(payload)}")
435
465
  if self.nodes.working_nodes_count == 0:
@@ -442,16 +472,8 @@ class GrapheneRPC(object):
442
472
  while True:
443
473
  self.nodes.increase_error_cnt_call()
444
474
  try:
445
- if (
446
- self.current_rpc == self.rpc_methods["ws"]
447
- or self.current_rpc == self.rpc_methods["wsappbase"]
448
- ):
449
- reply = self.ws_send(json.dumps(payload, ensure_ascii=False).encode("utf8"))
450
- else:
451
- response = self.request_send(
452
- json.dumps(payload, ensure_ascii=False).encode("utf8")
453
- )
454
- reply = response.text
475
+ response = self.request_send(json.dumps(payload, ensure_ascii=False).encode("utf8"))
476
+ reply = response.text
455
477
  if not bool(reply):
456
478
  try:
457
479
  self.nodes.sleep_and_check_retries("Empty Reply", call_retry=True)
@@ -465,18 +487,10 @@ class GrapheneRPC(object):
465
487
  break
466
488
  except KeyboardInterrupt:
467
489
  raise
468
- except WebSocketConnectionClosedException as e:
469
- self.nodes.increase_error_cnt()
470
- self.nodes.sleep_and_check_retries(str(e), sleep=False, call_retry=False)
471
- self.rpcconnect()
472
490
  except ConnectionError as e:
473
491
  self.nodes.increase_error_cnt()
474
492
  self.nodes.sleep_and_check_retries(str(e), sleep=False, call_retry=False)
475
493
  self.rpcconnect()
476
- except WebSocketTimeoutException as e:
477
- self.nodes.increase_error_cnt()
478
- self.nodes.sleep_and_check_retries(str(e), sleep=False, call_retry=False)
479
- self.rpcconnect()
480
494
  except Exception as e:
481
495
  self.nodes.increase_error_cnt()
482
496
  self.nodes.sleep_and_check_retries(str(e), sleep=False, call_retry=False)
nectarapi/noderpc.py CHANGED
@@ -44,13 +44,19 @@ class NodeRPC(GrapheneRPC):
44
44
  self.next_node_on_empty_reply = next_node_on_empty_reply
45
45
 
46
46
  def rpcexec(self, payload):
47
- """Execute a call by sending the payload.
48
- It makes use of the GrapheneRPC library.
49
- In here, we mostly deal with Steem specific error handling
47
+ """
48
+ Execute an RPC call with node-aware retry and Hive-specific error handling.
49
+
50
+ Sends the given JSON-RPC payload via the underlying GrapheneRPC implementation and handles node-level failures, automatic retries, and node switching when appropriate. If the instance flag `next_node_on_empty_reply` is set, an empty reply may trigger switching to the next node (when multiple nodes are available). Retries are governed by the node manager's retry policy.
51
+
52
+ Parameters:
53
+ payload (dict or list): JSON-RPC payload to send (method, params, id, etc.).
50
54
 
51
- :param json payload: Payload data
52
- :raises ValueError: if the server does not respond in proper JSON format
53
- :raises RPCError: if the server returns an error
55
+ Raises:
56
+ RPCConnection: if no RPC URL is configured (connection not established).
57
+ CallRetriesReached: when the node-manager's retry budget is exhausted and no alternative node can be used.
58
+ RPCError: when the remote node returns an RPC error that is not recoverable by retries/switching.
59
+ Exception: any other unexpected exception raised by the underlying RPC call is propagated.
54
60
  """
55
61
  if self.url is None:
56
62
  raise exceptions.RPCConnection("RPC is not connected!")
nectarapi/rpcutils.py CHANGED
@@ -6,12 +6,18 @@ log = logging.getLogger(__name__)
6
6
 
7
7
 
8
8
  def is_network_appbase_ready(props):
9
- """Checks if the network is appbase ready"""
10
- if "STEEMIT_BLOCKCHAIN_VERSION" in props:
11
- return False
12
- elif "STEEM_BLOCKCHAIN_VERSION" in props:
13
- return True
14
- elif "HIVE_BLOCKCHAIN_VERSION" in props:
9
+ """
10
+ Return True if the provided network/node properties indicate an appbase-ready node.
11
+
12
+ Checks for the presence of the "HIVE_BLOCKCHAIN_VERSION" key in props.
13
+
14
+ Parameters:
15
+ props (Mapping): Mapping (e.g., dict) of network or node properties.
16
+
17
+ Returns:
18
+ bool: True if "HIVE_BLOCKCHAIN_VERSION" exists in props, otherwise False.
19
+ """
20
+ if "HIVE_BLOCKCHAIN_VERSION" in props:
15
21
  return True
16
22
  else:
17
23
  return False
nectarapi/version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """THIS FILE IS GENERATED FROM nectar PYPROJECT.TOML."""
2
2
 
3
- version = "0.0.11"
3
+ version = "0.1.0"
@@ -45,9 +45,32 @@ class Ledger_Transaction(GrapheneUnsigned_Transaction):
45
45
  return Operation
46
46
 
47
47
  def getKnownChains(self):
48
+ """
49
+ Return the mapping of known blockchain chains available to this transaction.
50
+
51
+ Returns:
52
+ dict: A mapping where keys are chain identifiers (e.g., "HIVE", "STEEM" or custom names)
53
+ and values are the chain metadata/configuration that was registered with this transaction.
54
+ """
48
55
  return self.known_chains
49
56
 
50
- def sign(self, path="48'/13'/0'/0'/0'", chain="STEEM"):
57
+ def sign(self, path="48'/13'/0'/0'/0'", chain="HIVE"):
58
+ """
59
+ Sign the transaction using a Ledger device and attach the resulting signature to this transaction.
60
+
61
+ Builds APDUs for the given BIP32 path and blockchain chain identifier, sends them to a connected Ledger dongle, collects the final signature returned by the device, and stores it as the transaction's "signatures" entry.
62
+
63
+ Parameters:
64
+ path (str): BIP32 derivation path to use on the Ledger (default "48'/13'/0'/0'/0'").
65
+ chain (str): Chain identifier used when building APDUs (e.g., "HIVE" or "STEEM").
66
+
67
+ Returns:
68
+ Ledger_Transaction: self with `self.data["signatures"]` set to an Array containing the Ledger-produced Signature.
69
+
70
+ Notes:
71
+ - This method opens a connection to the Ledger device and closes it before returning.
72
+ - Any exceptions raised by the Ledger communication layer are not handled here and will propagate to the caller.
73
+ """
51
74
  from ledgerblue.comm import getDongle
52
75
 
53
76
  dongle = getDongle(True)
nectarbase/objects.py CHANGED
@@ -103,12 +103,23 @@ class Amount(object):
103
103
 
104
104
  def __bytes__(self):
105
105
  # padding
106
- # Workaround to allow transfers in HIVE
107
- if self.symbol == "HBD":
108
- self.symbol = "SBD"
109
- elif self.symbol == "HIVE":
110
- self.symbol = "STEEM"
111
- symbol = self.symbol + "\x00" * (7 - len(self.symbol))
106
+ # The nodes still serialize the legacy symbol name for HBD as 'SBD' and HIVE as 'STEEM' in wire format.
107
+ # To match get_transaction_hex and avoid digest mismatches, map 'HBD' -> 'SBD' and 'HIVE' -> 'STEEM' on serialization.
108
+ """
109
+ Serialize the Amount into its wire-format byte representation.
110
+
111
+ Returns:
112
+ bytes: 8-byte little-endian signed integer amount, followed by a 1-byte precision,
113
+ followed by a 7-byte ASCII symbol padded with null bytes. On serialization,
114
+ the symbol is remapped for legacy wire-format compatibility: "HBD" -> "SBD"
115
+ and "HIVE" -> "STEEM".
116
+ """
117
+ _sym = self.symbol
118
+ if _sym == "HBD":
119
+ _sym = "SBD"
120
+ elif _sym == "HIVE":
121
+ _sym = "STEEM"
122
+ symbol = _sym + "\x00" * (7 - len(_sym))
112
123
  return (
113
124
  struct.pack("<q", int(self.amount))
114
125
  + struct.pack("<b", self.precision)