hive-nectar 0.0.10__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.
- {hive_nectar-0.0.10.dist-info → hive_nectar-0.1.0.dist-info}/METADATA +10 -11
- hive_nectar-0.1.0.dist-info/RECORD +88 -0
- nectar/__init__.py +1 -4
- nectar/account.py +791 -685
- nectar/amount.py +82 -21
- nectar/asset.py +1 -2
- nectar/block.py +34 -22
- nectar/blockchain.py +111 -143
- nectar/blockchaininstance.py +396 -247
- nectar/blockchainobject.py +33 -5
- nectar/cli.py +1058 -1349
- nectar/comment.py +317 -182
- nectar/community.py +39 -43
- nectar/constants.py +1 -14
- nectar/discussions.py +793 -139
- nectar/hive.py +137 -77
- nectar/hivesigner.py +106 -68
- nectar/imageuploader.py +33 -23
- nectar/instance.py +31 -79
- nectar/market.py +128 -264
- nectar/memo.py +40 -13
- nectar/message.py +23 -10
- nectar/nodelist.py +118 -82
- nectar/price.py +80 -61
- nectar/profile.py +6 -3
- nectar/rc.py +45 -25
- nectar/snapshot.py +285 -163
- nectar/storage.py +16 -5
- nectar/transactionbuilder.py +132 -41
- nectar/utils.py +37 -17
- nectar/version.py +1 -1
- nectar/vote.py +171 -30
- nectar/wallet.py +26 -19
- nectar/witness.py +153 -54
- nectarapi/graphenerpc.py +147 -133
- nectarapi/noderpc.py +12 -6
- nectarapi/rpcutils.py +12 -6
- nectarapi/version.py +1 -1
- nectarbase/ledgertransactions.py +24 -1
- nectarbase/objects.py +17 -6
- nectarbase/operations.py +160 -90
- nectarbase/signedtransactions.py +38 -2
- nectarbase/version.py +1 -1
- nectargraphenebase/account.py +295 -17
- nectargraphenebase/chains.py +0 -135
- nectargraphenebase/ecdsasig.py +152 -176
- nectargraphenebase/types.py +18 -4
- nectargraphenebase/unsignedtransactions.py +1 -1
- nectargraphenebase/version.py +1 -1
- hive_nectar-0.0.10.dist-info/RECORD +0 -91
- nectar/blurt.py +0 -562
- nectar/conveyor.py +0 -308
- nectar/steem.py +0 -581
- {hive_nectar-0.0.10.dist-info → hive_nectar-0.1.0.dist-info}/WHEEL +0 -0
- {hive_nectar-0.0.10.dist-info → hive_nectar-0.1.0.dist-info}/entry_points.txt +0 -0
- {hive_nectar-0.0.10.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
|
-
|
|
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
|
-
"""
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
"""
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
"""
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
147
|
+
return self.current_rpc == self.rpc_methods["appbase"]
|
|
162
148
|
|
|
163
149
|
def get_use_appbase(self):
|
|
164
|
-
"""
|
|
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
|
-
"""
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
self.
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
238
|
-
"""
|
|
239
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
279
|
-
|
|
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
|
-
|
|
302
|
-
|
|
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
|
|
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
|
-
:
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
-
"""
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
:
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
"""
|
|
10
|
-
if
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
nectarbase/ledgertransactions.py
CHANGED
|
@@ -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="
|
|
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
|
-
#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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)
|