hive-nectar 0.0.2__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.2.dist-info/METADATA +182 -0
- hive_nectar-0.0.2.dist-info/RECORD +86 -0
- hive_nectar-0.0.2.dist-info/WHEEL +4 -0
- hive_nectar-0.0.2.dist-info/entry_points.txt +2 -0
- hive_nectar-0.0.2.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +32 -0
- nectar/account.py +4371 -0
- nectar/amount.py +475 -0
- nectar/asciichart.py +270 -0
- nectar/asset.py +82 -0
- nectar/block.py +446 -0
- nectar/blockchain.py +1178 -0
- nectar/blockchaininstance.py +2284 -0
- nectar/blockchainobject.py +221 -0
- nectar/blurt.py +563 -0
- nectar/cli.py +6285 -0
- nectar/comment.py +1217 -0
- nectar/community.py +513 -0
- nectar/constants.py +111 -0
- nectar/conveyor.py +309 -0
- nectar/discussions.py +1709 -0
- nectar/exceptions.py +149 -0
- nectar/hive.py +546 -0
- nectar/hivesigner.py +420 -0
- nectar/imageuploader.py +72 -0
- nectar/instance.py +129 -0
- nectar/market.py +1013 -0
- nectar/memo.py +449 -0
- nectar/message.py +357 -0
- nectar/nodelist.py +444 -0
- nectar/price.py +557 -0
- nectar/profile.py +65 -0
- nectar/rc.py +308 -0
- nectar/snapshot.py +726 -0
- nectar/steem.py +582 -0
- nectar/storage.py +53 -0
- nectar/transactionbuilder.py +622 -0
- nectar/utils.py +545 -0
- nectar/version.py +2 -0
- nectar/vote.py +557 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +617 -0
- nectarapi/__init__.py +11 -0
- nectarapi/exceptions.py +123 -0
- nectarapi/graphenerpc.py +589 -0
- nectarapi/node.py +178 -0
- nectarapi/noderpc.py +229 -0
- nectarapi/rpcutils.py +97 -0
- nectarapi/version.py +2 -0
- nectarbase/__init__.py +14 -0
- nectarbase/ledgertransactions.py +75 -0
- nectarbase/memo.py +243 -0
- nectarbase/objects.py +429 -0
- nectarbase/objecttypes.py +22 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1297 -0
- nectarbase/signedtransactions.py +48 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +2 -0
- nectargrapheneapi/__init__.py +6 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +846 -0
- nectargraphenebase/aes.py +52 -0
- nectargraphenebase/base58.py +192 -0
- nectargraphenebase/bip32.py +494 -0
- nectargraphenebase/bip38.py +134 -0
- nectargraphenebase/chains.py +149 -0
- nectargraphenebase/dictionary.py +3 -0
- nectargraphenebase/ecdsasig.py +326 -0
- nectargraphenebase/objects.py +123 -0
- nectargraphenebase/objecttypes.py +6 -0
- nectargraphenebase/operationids.py +3 -0
- nectargraphenebase/operations.py +23 -0
- nectargraphenebase/prefix.py +11 -0
- nectargraphenebase/py23.py +38 -0
- nectargraphenebase/signedtransactions.py +201 -0
- nectargraphenebase/types.py +419 -0
- nectargraphenebase/unsignedtransactions.py +283 -0
- nectargraphenebase/version.py +2 -0
- nectarstorage/__init__.py +38 -0
- nectarstorage/base.py +306 -0
- nectarstorage/exceptions.py +16 -0
- nectarstorage/interfaces.py +237 -0
- nectarstorage/masterpassword.py +239 -0
- nectarstorage/ram.py +30 -0
- nectarstorage/sqlite.py +334 -0
nectarapi/node.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from .exceptions import CallRetriesReached, NumRetriesReached
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Node(object):
|
|
12
|
+
def __init__(self, url):
|
|
13
|
+
self.url = url
|
|
14
|
+
self.error_cnt = 0
|
|
15
|
+
self.error_cnt_call = 0
|
|
16
|
+
|
|
17
|
+
def __repr__(self):
|
|
18
|
+
return self.url
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Nodes(list):
|
|
22
|
+
"""Stores Node URLs and error counts"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, urls, num_retries, num_retries_call):
|
|
25
|
+
self.set_node_urls(urls)
|
|
26
|
+
self.num_retries = num_retries
|
|
27
|
+
self.num_retries_call = num_retries_call
|
|
28
|
+
|
|
29
|
+
def set_node_urls(self, urls):
|
|
30
|
+
if isinstance(urls, str):
|
|
31
|
+
url_list = re.split(r",|;", urls)
|
|
32
|
+
if url_list is None:
|
|
33
|
+
url_list = [urls]
|
|
34
|
+
elif isinstance(urls, Nodes):
|
|
35
|
+
url_list = [urls[i].url for i in range(len(urls))]
|
|
36
|
+
elif isinstance(urls, (list, tuple, set)):
|
|
37
|
+
url_list = urls
|
|
38
|
+
elif urls is not None:
|
|
39
|
+
url_list = [urls]
|
|
40
|
+
else:
|
|
41
|
+
url_list = []
|
|
42
|
+
super(Nodes, self).__init__([Node(x) for x in url_list])
|
|
43
|
+
self.current_node_index = -1
|
|
44
|
+
self.freeze_current_node = False
|
|
45
|
+
|
|
46
|
+
def __iter__(self):
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def __next__(self):
|
|
50
|
+
next_node_count = 0
|
|
51
|
+
if self.freeze_current_node:
|
|
52
|
+
return self.url
|
|
53
|
+
while next_node_count == 0 and (
|
|
54
|
+
self.num_retries < 0 or self.node.error_cnt < self.num_retries
|
|
55
|
+
):
|
|
56
|
+
self.current_node_index += 1
|
|
57
|
+
if self.current_node_index >= self.working_nodes_count:
|
|
58
|
+
self.current_node_index = 0
|
|
59
|
+
next_node_count += 1
|
|
60
|
+
if next_node_count > self.working_nodes_count + 1:
|
|
61
|
+
raise StopIteration
|
|
62
|
+
return self.url
|
|
63
|
+
|
|
64
|
+
next = __next__ # Python 2
|
|
65
|
+
|
|
66
|
+
def export_working_nodes(self):
|
|
67
|
+
nodes_list = []
|
|
68
|
+
for i in range(len(self)):
|
|
69
|
+
if self.num_retries < 0 or self[i].error_cnt <= self.num_retries:
|
|
70
|
+
nodes_list.append(self[i].url)
|
|
71
|
+
return nodes_list
|
|
72
|
+
|
|
73
|
+
def __repr__(self):
|
|
74
|
+
nodes_list = self.export_working_nodes()
|
|
75
|
+
return str(nodes_list)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def working_nodes_count(self):
|
|
79
|
+
n = 0
|
|
80
|
+
if self.freeze_current_node:
|
|
81
|
+
i = self.current_node_index
|
|
82
|
+
if self.current_node_index < 0:
|
|
83
|
+
i = 0
|
|
84
|
+
if self.num_retries < 0 or self[i].error_cnt <= self.num_retries:
|
|
85
|
+
n += 1
|
|
86
|
+
return n
|
|
87
|
+
for i in range(len(self)):
|
|
88
|
+
if self.num_retries < 0 or self[i].error_cnt <= self.num_retries:
|
|
89
|
+
n += 1
|
|
90
|
+
return n
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def url(self):
|
|
94
|
+
if self.node is None:
|
|
95
|
+
return ""
|
|
96
|
+
return self.node.url
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def node(self):
|
|
100
|
+
if self.current_node_index < 0:
|
|
101
|
+
return self[0]
|
|
102
|
+
return self[self.current_node_index]
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def error_cnt(self):
|
|
106
|
+
if self.node is None:
|
|
107
|
+
return 0
|
|
108
|
+
return self.node.error_cnt
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def error_cnt_call(self):
|
|
112
|
+
if self.node is None:
|
|
113
|
+
return 0
|
|
114
|
+
return self.node.error_cnt_call
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def num_retries_call_reached(self):
|
|
118
|
+
return self.error_cnt_call >= self.num_retries_call
|
|
119
|
+
|
|
120
|
+
def disable_node(self):
|
|
121
|
+
"""Disable current node"""
|
|
122
|
+
if self.node is not None and self.num_retries_call >= 0:
|
|
123
|
+
self.node.error_cnt_call = self.num_retries_call
|
|
124
|
+
|
|
125
|
+
def increase_error_cnt(self):
|
|
126
|
+
"""Increase node error count for current node"""
|
|
127
|
+
if self.node is not None:
|
|
128
|
+
self.node.error_cnt += 1
|
|
129
|
+
|
|
130
|
+
def increase_error_cnt_call(self):
|
|
131
|
+
"""Increase call error count for current node"""
|
|
132
|
+
if self.node is not None:
|
|
133
|
+
self.node.error_cnt_call += 1
|
|
134
|
+
|
|
135
|
+
def reset_error_cnt_call(self):
|
|
136
|
+
"""Set call error count for current node to zero"""
|
|
137
|
+
if self.node is not None:
|
|
138
|
+
self.node.error_cnt_call = 0
|
|
139
|
+
|
|
140
|
+
def reset_error_cnt(self):
|
|
141
|
+
"""Set node error count for current node to zero"""
|
|
142
|
+
if self.node is not None:
|
|
143
|
+
self.node.error_cnt = 0
|
|
144
|
+
|
|
145
|
+
def sleep_and_check_retries(self, errorMsg=None, sleep=True, call_retry=False, showMsg=True):
|
|
146
|
+
"""Sleep and check if num_retries is reached"""
|
|
147
|
+
if errorMsg:
|
|
148
|
+
log.warning("Error: {}".format(errorMsg))
|
|
149
|
+
if call_retry:
|
|
150
|
+
cnt = self.error_cnt_call
|
|
151
|
+
if self.num_retries_call >= 0 and self.error_cnt_call > self.num_retries_call:
|
|
152
|
+
raise CallRetriesReached()
|
|
153
|
+
else:
|
|
154
|
+
cnt = self.error_cnt
|
|
155
|
+
if self.num_retries >= 0 and self.error_cnt > self.num_retries:
|
|
156
|
+
raise NumRetriesReached()
|
|
157
|
+
|
|
158
|
+
if showMsg:
|
|
159
|
+
if call_retry:
|
|
160
|
+
log.warning(
|
|
161
|
+
"Retry RPC Call on node: %s (%d/%d) \n" % (self.url, cnt, self.num_retries_call)
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
log.warning(
|
|
165
|
+
"Lost connection or internal error on node: %s (%d/%d) \n"
|
|
166
|
+
% (self.url, cnt, self.num_retries)
|
|
167
|
+
)
|
|
168
|
+
if not sleep:
|
|
169
|
+
return
|
|
170
|
+
if cnt < 1:
|
|
171
|
+
sleeptime = 0
|
|
172
|
+
elif cnt < 10:
|
|
173
|
+
sleeptime = (cnt - 1) * 1.5 + 0.5
|
|
174
|
+
else:
|
|
175
|
+
sleeptime = 10
|
|
176
|
+
if sleeptime:
|
|
177
|
+
log.warning("Retrying in %d seconds\n" % sleeptime)
|
|
178
|
+
time.sleep(sleeptime)
|
nectarapi/noderpc.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from . import exceptions
|
|
6
|
+
from .graphenerpc import GrapheneRPC
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class NodeRPC(GrapheneRPC):
|
|
12
|
+
"""This class allows to call API methods exposed by the witness node via
|
|
13
|
+
websockets / rpc-json.
|
|
14
|
+
|
|
15
|
+
:param str urls: Either a single Websocket/Http URL, or a list of URLs
|
|
16
|
+
:param str user: Username for Authentication
|
|
17
|
+
:param str password: Password for Authentication
|
|
18
|
+
:param int num_retries: Try x times to num_retries to a node on disconnect, -1 for indefinitely
|
|
19
|
+
:param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5)
|
|
20
|
+
:param int timeout: Timeout setting for https nodes (default is 60)
|
|
21
|
+
:param bool use_condenser: Use the old condenser_api rpc protocol on nodes with version
|
|
22
|
+
0.19.4 or higher. The settings has no effect on nodes with version of 0.19.3 or lower.
|
|
23
|
+
:param bool use_tor: When set to true, 'socks5h://localhost:9050' is set as proxy
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, *args, **kwargs):
|
|
28
|
+
"""Init NodeRPC
|
|
29
|
+
|
|
30
|
+
:param str urls: Either a single Websocket/Http URL, or a list of URLs
|
|
31
|
+
:param str user: Username for Authentication
|
|
32
|
+
:param str password: Password for Authentication
|
|
33
|
+
:param int num_retries: Try x times to num_retries to a node on disconnect, -1 for indefinitely
|
|
34
|
+
:param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5)
|
|
35
|
+
:param int timeout: Timeout setting for https nodes (default is 60)
|
|
36
|
+
:param bool use_tor: When set to true, 'socks5h://localhost:9050' is set as proxy
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
super(NodeRPC, self).__init__(*args, **kwargs)
|
|
40
|
+
self.next_node_on_empty_reply = False
|
|
41
|
+
|
|
42
|
+
def set_next_node_on_empty_reply(self, next_node_on_empty_reply=True):
|
|
43
|
+
"""Switch to next node on empty reply for the next rpc call"""
|
|
44
|
+
self.next_node_on_empty_reply = next_node_on_empty_reply
|
|
45
|
+
|
|
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
|
|
50
|
+
|
|
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
|
|
54
|
+
"""
|
|
55
|
+
if self.url is None:
|
|
56
|
+
raise exceptions.RPCConnection("RPC is not connected!")
|
|
57
|
+
doRetry = True
|
|
58
|
+
maxRetryCountReached = False
|
|
59
|
+
while doRetry and not maxRetryCountReached:
|
|
60
|
+
doRetry = False
|
|
61
|
+
try:
|
|
62
|
+
# Forward call to GrapheneWebsocketRPC and catch+evaluate errors
|
|
63
|
+
reply = super(NodeRPC, self).rpcexec(payload)
|
|
64
|
+
if (
|
|
65
|
+
self.next_node_on_empty_reply
|
|
66
|
+
and not bool(reply)
|
|
67
|
+
and self.nodes.working_nodes_count > 1
|
|
68
|
+
):
|
|
69
|
+
self._retry_on_next_node("Empty Reply")
|
|
70
|
+
doRetry = True
|
|
71
|
+
self.next_node_on_empty_reply = True
|
|
72
|
+
else:
|
|
73
|
+
self.next_node_on_empty_reply = False
|
|
74
|
+
return reply
|
|
75
|
+
except exceptions.RPCErrorDoRetry as e:
|
|
76
|
+
msg = exceptions.decodeRPCErrorMsg(e).strip()
|
|
77
|
+
try:
|
|
78
|
+
self.nodes.sleep_and_check_retries(str(msg), call_retry=True)
|
|
79
|
+
doRetry = True
|
|
80
|
+
except exceptions.CallRetriesReached:
|
|
81
|
+
if self.nodes.working_nodes_count > 1:
|
|
82
|
+
self._retry_on_next_node(msg)
|
|
83
|
+
doRetry = True
|
|
84
|
+
else:
|
|
85
|
+
self.next_node_on_empty_reply = False
|
|
86
|
+
raise exceptions.CallRetriesReached
|
|
87
|
+
except exceptions.RPCError as e:
|
|
88
|
+
try:
|
|
89
|
+
doRetry = self._check_error_message(e, self.error_cnt_call)
|
|
90
|
+
except exceptions.CallRetriesReached:
|
|
91
|
+
msg = exceptions.decodeRPCErrorMsg(e).strip()
|
|
92
|
+
if self.nodes.working_nodes_count > 1:
|
|
93
|
+
self._retry_on_next_node(msg)
|
|
94
|
+
doRetry = True
|
|
95
|
+
else:
|
|
96
|
+
self.next_node_on_empty_reply = False
|
|
97
|
+
raise exceptions.CallRetriesReached
|
|
98
|
+
except Exception as e:
|
|
99
|
+
self.next_node_on_empty_reply = False
|
|
100
|
+
raise e
|
|
101
|
+
maxRetryCountReached = self.nodes.num_retries_call_reached
|
|
102
|
+
self.next_node_on_empty_reply = False
|
|
103
|
+
|
|
104
|
+
def _retry_on_next_node(self, error_msg):
|
|
105
|
+
self.nodes.increase_error_cnt()
|
|
106
|
+
self.nodes.sleep_and_check_retries(error_msg, sleep=False, call_retry=False)
|
|
107
|
+
self.next()
|
|
108
|
+
|
|
109
|
+
def _check_error_message(self, e, cnt):
|
|
110
|
+
"""Check error message and decide what to do"""
|
|
111
|
+
doRetry = False
|
|
112
|
+
msg = exceptions.decodeRPCErrorMsg(e).strip()
|
|
113
|
+
if re.search("missing required active authority", msg):
|
|
114
|
+
raise exceptions.MissingRequiredActiveAuthority
|
|
115
|
+
elif re.match("^no method with name.*", msg):
|
|
116
|
+
raise exceptions.NoMethodWithName(msg)
|
|
117
|
+
elif re.search("Could not find method", msg):
|
|
118
|
+
raise exceptions.NoMethodWithName(msg)
|
|
119
|
+
elif re.search("Unknown Transaction", msg):
|
|
120
|
+
raise exceptions.UnknownTransaction(msg)
|
|
121
|
+
elif re.search("Could not find API", msg):
|
|
122
|
+
if self._check_api_name(msg):
|
|
123
|
+
if self.nodes.working_nodes_count > 1 and self.nodes.num_retries > -1:
|
|
124
|
+
self.nodes.disable_node()
|
|
125
|
+
self._switch_to_next_node(msg, "ApiNotSupported")
|
|
126
|
+
doRetry = True
|
|
127
|
+
else:
|
|
128
|
+
raise exceptions.ApiNotSupported(msg)
|
|
129
|
+
else:
|
|
130
|
+
raise exceptions.NoApiWithName(msg)
|
|
131
|
+
elif re.search("follow_api_plugin not enabled", msg):
|
|
132
|
+
if self.nodes.working_nodes_count > 1 and self.nodes.num_retries > -1:
|
|
133
|
+
self._switch_to_next_node(str(e))
|
|
134
|
+
doRetry = True
|
|
135
|
+
else:
|
|
136
|
+
raise exceptions.FollowApiNotEnabled(msg)
|
|
137
|
+
elif re.search("irrelevant signature included", msg):
|
|
138
|
+
raise exceptions.UnnecessarySignatureDetected(msg)
|
|
139
|
+
elif re.search("WinError", msg):
|
|
140
|
+
raise exceptions.RPCError(msg)
|
|
141
|
+
elif re.search("Invalid parameters", msg):
|
|
142
|
+
raise exceptions.InvalidParameters()
|
|
143
|
+
elif re.search("Supported by Hivemind", msg):
|
|
144
|
+
raise exceptions.SupportedByHivemind()
|
|
145
|
+
elif re.search("Could not find filtered operation", msg):
|
|
146
|
+
raise exceptions.FilteredItemNotFound(msg)
|
|
147
|
+
elif re.search("Unable to acquire database lock", msg):
|
|
148
|
+
self.nodes.sleep_and_check_retries(str(msg), call_retry=True)
|
|
149
|
+
doRetry = True
|
|
150
|
+
elif re.search("Request Timeout", msg):
|
|
151
|
+
self.nodes.sleep_and_check_retries(str(msg), call_retry=True)
|
|
152
|
+
doRetry = True
|
|
153
|
+
elif re.search("Bad or missing upstream response", msg):
|
|
154
|
+
self.nodes.sleep_and_check_retries(str(msg), call_retry=True)
|
|
155
|
+
doRetry = True
|
|
156
|
+
elif re.search("Internal Error", msg) or re.search("Unknown exception", msg):
|
|
157
|
+
self.nodes.sleep_and_check_retries(str(msg), call_retry=True)
|
|
158
|
+
doRetry = True
|
|
159
|
+
elif re.search("!check_max_block_age", str(e)):
|
|
160
|
+
self._switch_to_next_node(str(e))
|
|
161
|
+
doRetry = True
|
|
162
|
+
elif re.search("Server error", str(e)):
|
|
163
|
+
self._switch_to_next_node(str(e))
|
|
164
|
+
doRetry = True
|
|
165
|
+
elif re.search("Can only vote once every 3 seconds", msg):
|
|
166
|
+
raise exceptions.VotedBeforeWaitTimeReached(msg)
|
|
167
|
+
elif re.search("out_of_rangeEEEE: unknown key", msg) or re.search(
|
|
168
|
+
"unknown key:unknown key", msg
|
|
169
|
+
):
|
|
170
|
+
raise exceptions.UnkownKey(msg)
|
|
171
|
+
elif re.search("Assert Exception:v.is_object(): Input data have to treated as object", msg):
|
|
172
|
+
raise exceptions.UnhandledRPCError(
|
|
173
|
+
"Use Operation(op, appbase=True) to prevent error: " + msg
|
|
174
|
+
)
|
|
175
|
+
elif re.search("Client returned invalid format. Expected JSON!", msg):
|
|
176
|
+
if self.nodes.working_nodes_count > 1 and self.nodes.num_retries > -1:
|
|
177
|
+
self.nodes.disable_node()
|
|
178
|
+
self._switch_to_next_node(msg)
|
|
179
|
+
doRetry = True
|
|
180
|
+
else:
|
|
181
|
+
raise exceptions.UnhandledRPCError(msg)
|
|
182
|
+
elif msg:
|
|
183
|
+
raise exceptions.UnhandledRPCError(msg)
|
|
184
|
+
else:
|
|
185
|
+
raise e
|
|
186
|
+
return doRetry
|
|
187
|
+
|
|
188
|
+
def _switch_to_next_node(self, msg, error_type="UnhandledRPCError"):
|
|
189
|
+
if self.nodes.working_nodes_count == 1:
|
|
190
|
+
if error_type == "UnhandledRPCError":
|
|
191
|
+
raise exceptions.UnhandledRPCError(msg)
|
|
192
|
+
elif error_type == "ApiNotSupported":
|
|
193
|
+
raise exceptions.ApiNotSupported(msg)
|
|
194
|
+
self.nodes.increase_error_cnt()
|
|
195
|
+
self.nodes.sleep_and_check_retries(str(msg), sleep=False)
|
|
196
|
+
self.next()
|
|
197
|
+
|
|
198
|
+
def _check_api_name(self, msg):
|
|
199
|
+
error_start = "Could not find API"
|
|
200
|
+
known_apis = [
|
|
201
|
+
"account_history_api",
|
|
202
|
+
"tags_api",
|
|
203
|
+
"database_api",
|
|
204
|
+
"market_history_api",
|
|
205
|
+
"block_api",
|
|
206
|
+
"account_by_key_api",
|
|
207
|
+
"chain_api",
|
|
208
|
+
"follow_api",
|
|
209
|
+
"condenser_api",
|
|
210
|
+
"debug_node_api",
|
|
211
|
+
"witness_api",
|
|
212
|
+
"test_api",
|
|
213
|
+
"bridge",
|
|
214
|
+
"network_broadcast_api",
|
|
215
|
+
]
|
|
216
|
+
for api in known_apis:
|
|
217
|
+
if re.search(error_start + " " + api, msg):
|
|
218
|
+
return True
|
|
219
|
+
if msg[-18:] == error_start:
|
|
220
|
+
return True
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def get_account(self, name, **kwargs):
|
|
224
|
+
"""Get full account details from account name
|
|
225
|
+
|
|
226
|
+
:param str name: Account name
|
|
227
|
+
"""
|
|
228
|
+
if isinstance(name, str):
|
|
229
|
+
return self.get_accounts([name], **kwargs)
|
nectarapi/rpcutils.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
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:
|
|
15
|
+
return True
|
|
16
|
+
else:
|
|
17
|
+
return False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_query(appbase, request_id, api_name, name, args):
|
|
21
|
+
query = []
|
|
22
|
+
if not appbase or api_name == "condenser_api":
|
|
23
|
+
query = {
|
|
24
|
+
"method": "call",
|
|
25
|
+
"params": [api_name, name, list(args)],
|
|
26
|
+
"jsonrpc": "2.0",
|
|
27
|
+
"id": request_id,
|
|
28
|
+
}
|
|
29
|
+
else:
|
|
30
|
+
args = json.loads(json.dumps(args))
|
|
31
|
+
# print(args)
|
|
32
|
+
if len(args) > 0 and isinstance(args, list) and isinstance(args[0], dict):
|
|
33
|
+
query = {
|
|
34
|
+
"method": api_name + "." + name,
|
|
35
|
+
"params": args[0],
|
|
36
|
+
"jsonrpc": "2.0",
|
|
37
|
+
"id": request_id,
|
|
38
|
+
}
|
|
39
|
+
elif (
|
|
40
|
+
len(args) > 0
|
|
41
|
+
and isinstance(args, list)
|
|
42
|
+
and isinstance(args[0], list)
|
|
43
|
+
and len(args[0]) > 0
|
|
44
|
+
and isinstance(args[0][0], dict)
|
|
45
|
+
):
|
|
46
|
+
for a in args[0]:
|
|
47
|
+
query.append(
|
|
48
|
+
{
|
|
49
|
+
"method": api_name + "." + name,
|
|
50
|
+
"params": a,
|
|
51
|
+
"jsonrpc": "2.0",
|
|
52
|
+
"id": request_id,
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
request_id += 1
|
|
56
|
+
elif args:
|
|
57
|
+
query = {
|
|
58
|
+
"method": "call",
|
|
59
|
+
"params": [api_name, name, list(args)],
|
|
60
|
+
"jsonrpc": "2.0",
|
|
61
|
+
"id": request_id,
|
|
62
|
+
}
|
|
63
|
+
request_id += 1
|
|
64
|
+
elif api_name == "condenser_api":
|
|
65
|
+
query = {
|
|
66
|
+
"method": api_name + "." + name,
|
|
67
|
+
"jsonrpc": "2.0",
|
|
68
|
+
"params": [],
|
|
69
|
+
"id": request_id,
|
|
70
|
+
}
|
|
71
|
+
else:
|
|
72
|
+
query = {
|
|
73
|
+
"method": api_name + "." + name,
|
|
74
|
+
"jsonrpc": "2.0",
|
|
75
|
+
"params": {},
|
|
76
|
+
"id": request_id,
|
|
77
|
+
}
|
|
78
|
+
return query
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_api_name(appbase, *args, **kwargs):
|
|
82
|
+
if not appbase:
|
|
83
|
+
# Sepcify the api to talk to
|
|
84
|
+
if ("api" in kwargs) and len(kwargs["api"]) > 0:
|
|
85
|
+
api_name = kwargs["api"].replace("_api", "") + "_api"
|
|
86
|
+
else:
|
|
87
|
+
api_name = None
|
|
88
|
+
else:
|
|
89
|
+
# Sepcify the api to talk to
|
|
90
|
+
if ("api" in kwargs) and len(kwargs["api"]) > 0:
|
|
91
|
+
if kwargs["api"] not in ["jsonrpc", "hive", "bridge"]:
|
|
92
|
+
api_name = kwargs["api"].replace("_api", "") + "_api"
|
|
93
|
+
else:
|
|
94
|
+
api_name = kwargs["api"]
|
|
95
|
+
else:
|
|
96
|
+
api_name = "condenser_api"
|
|
97
|
+
return api_name
|
nectarapi/version.py
ADDED
nectarbase/__init__.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from nectargraphenebase.account import PublicKey
|
|
5
|
+
from nectargraphenebase.chains import known_chains
|
|
6
|
+
from nectargraphenebase.py23 import py23_bytes
|
|
7
|
+
from nectargraphenebase.types import (
|
|
8
|
+
Array,
|
|
9
|
+
Signature,
|
|
10
|
+
)
|
|
11
|
+
from nectargraphenebase.unsignedtransactions import (
|
|
12
|
+
Unsigned_Transaction as GrapheneUnsigned_Transaction,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
from .operations import Operation
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Ledger_Transaction(GrapheneUnsigned_Transaction):
|
|
21
|
+
"""Create an unsigned transaction and offer method to send it to a ledger device for signing
|
|
22
|
+
|
|
23
|
+
:param num ref_block_num:
|
|
24
|
+
:param num ref_block_prefix:
|
|
25
|
+
:param str expiration: expiration date
|
|
26
|
+
:param array operations: array of operations
|
|
27
|
+
:param dict custom_chains: custom chain which should be added to the known chains
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, *args, **kwargs):
|
|
31
|
+
self.known_chains = known_chains
|
|
32
|
+
custom_chain = kwargs.get("custom_chains", {})
|
|
33
|
+
if len(custom_chain) > 0:
|
|
34
|
+
for c in custom_chain:
|
|
35
|
+
if c not in self.known_chains:
|
|
36
|
+
self.known_chains[c] = custom_chain[c]
|
|
37
|
+
super(Ledger_Transaction, self).__init__(*args, **kwargs)
|
|
38
|
+
|
|
39
|
+
def add_custom_chains(self, custom_chain):
|
|
40
|
+
if len(custom_chain) > 0:
|
|
41
|
+
for c in custom_chain:
|
|
42
|
+
if c not in self.known_chains:
|
|
43
|
+
self.known_chains[c] = custom_chain[c]
|
|
44
|
+
|
|
45
|
+
def getOperationKlass(self):
|
|
46
|
+
return Operation
|
|
47
|
+
|
|
48
|
+
def getKnownChains(self):
|
|
49
|
+
return self.known_chains
|
|
50
|
+
|
|
51
|
+
def sign(self, path="48'/13'/0'/0'/0'", chain="STEEM"):
|
|
52
|
+
from ledgerblue.comm import getDongle
|
|
53
|
+
|
|
54
|
+
dongle = getDongle(True)
|
|
55
|
+
apdu_list = self.build_apdu(path, chain)
|
|
56
|
+
for apdu in apdu_list:
|
|
57
|
+
result = dongle.exchange(py23_bytes(apdu))
|
|
58
|
+
dongle.close()
|
|
59
|
+
sigs = []
|
|
60
|
+
signature = result
|
|
61
|
+
sigs.append(Signature(signature))
|
|
62
|
+
self.data["signatures"] = Array(sigs)
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
def get_pubkey(self, path="48'/13'/0'/0'/0'", request_screen_approval=False, prefix="STM"):
|
|
66
|
+
from ledgerblue.comm import getDongle
|
|
67
|
+
|
|
68
|
+
dongle = getDongle(True)
|
|
69
|
+
apdu = self.build_apdu_pubkey(path, request_screen_approval)
|
|
70
|
+
result = dongle.exchange(py23_bytes(apdu))
|
|
71
|
+
dongle.close()
|
|
72
|
+
offset = 1 + result[0]
|
|
73
|
+
address = result[offset + 1 : offset + 1 + result[offset]]
|
|
74
|
+
# public_key = result[1: 1 + result[0]]
|
|
75
|
+
return PublicKey(address.decode(), prefix=prefix)
|