hive-nectar 0.1.1__py3-none-any.whl → 0.1.3__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.1.1.dist-info → hive_nectar-0.1.3.dist-info}/METADATA +1 -2
- {hive_nectar-0.1.1.dist-info → hive_nectar-0.1.3.dist-info}/RECORD +22 -21
- nectar/account.py +86 -35
- nectar/block.py +3 -3
- nectar/blockchain.py +2 -2
- nectar/blockchaininstance.py +11 -1
- nectar/cli.py +292 -266
- nectar/comment.py +19 -16
- nectar/haf.py +380 -0
- nectar/hivesigner.py +9 -0
- nectar/market.py +2 -4
- nectar/memo.py +2 -2
- nectar/nodelist.py +0 -24
- nectar/utils.py +60 -37
- nectar/version.py +1 -1
- nectarapi/version.py +1 -1
- nectarbase/version.py +1 -1
- nectargraphenebase/account.py +1 -2
- nectargraphenebase/version.py +1 -1
- {hive_nectar-0.1.1.dist-info → hive_nectar-0.1.3.dist-info}/WHEEL +0 -0
- {hive_nectar-0.1.1.dist-info → hive_nectar-0.1.3.dist-info}/entry_points.txt +0 -0
- {hive_nectar-0.1.1.dist-info → hive_nectar-0.1.3.dist-info}/licenses/LICENSE.txt +0 -0
nectar/comment.py
CHANGED
|
@@ -43,7 +43,7 @@ class Comment(BlockchainObject):
|
|
|
43
43
|
>>> from nectar.comment import Comment
|
|
44
44
|
>>> from nectar.account import Account
|
|
45
45
|
>>> # Create a Hive blockchain instance
|
|
46
|
-
>>> from nectar
|
|
46
|
+
>>> from nectar import Hive
|
|
47
47
|
>>> hv = Hive()
|
|
48
48
|
>>> acc = Account("gtg", blockchain_instance=hv)
|
|
49
49
|
>>> authorperm = acc.get_blog(limit=1)[0]["authorperm"]
|
|
@@ -467,9 +467,9 @@ class Comment(BlockchainObject):
|
|
|
467
467
|
Amount: Estimated HBD payout required to offset the early-vote curation penalty.
|
|
468
468
|
"""
|
|
469
469
|
self.refresh()
|
|
470
|
-
if self.blockchain.hardfork
|
|
470
|
+
if self.blockchain.hardfork >= 21:
|
|
471
471
|
reverse_auction_window_seconds = HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF21
|
|
472
|
-
elif self.blockchain.hardfork
|
|
472
|
+
elif self.blockchain.hardfork >= 20:
|
|
473
473
|
reverse_auction_window_seconds = HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF20
|
|
474
474
|
else:
|
|
475
475
|
reverse_auction_window_seconds = HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF6
|
|
@@ -526,9 +526,9 @@ class Comment(BlockchainObject):
|
|
|
526
526
|
elapsed_seconds = (vote_time - self["created"]).total_seconds()
|
|
527
527
|
else:
|
|
528
528
|
raise ValueError("vote_time must be a string or a datetime")
|
|
529
|
-
if self.blockchain.hardfork
|
|
529
|
+
if self.blockchain.hardfork >= 21:
|
|
530
530
|
reward = elapsed_seconds / HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF21
|
|
531
|
-
elif self.blockchain.hardfork
|
|
531
|
+
elif self.blockchain.hardfork >= 20:
|
|
532
532
|
reward = elapsed_seconds / HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF20
|
|
533
533
|
else:
|
|
534
534
|
reward = elapsed_seconds / HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF6
|
|
@@ -675,7 +675,7 @@ class Comment(BlockchainObject):
|
|
|
675
675
|
curation_tokens = self.reward * author_reward_factor
|
|
676
676
|
author_tokens = self.reward - curation_tokens
|
|
677
677
|
curation_rewards = self.get_curation_rewards()
|
|
678
|
-
if self.blockchain.hardfork
|
|
678
|
+
if self.blockchain.hardfork >= 20 and median_hist is not None:
|
|
679
679
|
author_tokens += median_price * curation_rewards["unclaimed_rewards"]
|
|
680
680
|
benefactor_tokens = author_tokens * beneficiaries_pct / HIVE_100_PERCENT
|
|
681
681
|
author_tokens -= benefactor_tokens
|
|
@@ -1209,9 +1209,10 @@ class RankedPosts(list):
|
|
|
1209
1209
|
if not self.blockchain.is_connected():
|
|
1210
1210
|
return None
|
|
1211
1211
|
comments = []
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1212
|
+
# Bridge API enforces a maximum page size (typically 20). Cap
|
|
1213
|
+
# per-request size accordingly and page until `limit` is reached.
|
|
1214
|
+
# Previously capped to 100 which can trigger Invalid parameters.
|
|
1215
|
+
api_limit = min(limit, 20)
|
|
1215
1216
|
last_n = -1
|
|
1216
1217
|
while len(comments) < limit and last_n != len(comments):
|
|
1217
1218
|
last_n = len(comments)
|
|
@@ -1248,8 +1249,9 @@ class RankedPosts(list):
|
|
|
1248
1249
|
if len(comments) > 0:
|
|
1249
1250
|
start_author = comments[-1]["author"]
|
|
1250
1251
|
start_permlink = comments[-1]["permlink"]
|
|
1251
|
-
|
|
1252
|
-
|
|
1252
|
+
# Recompute per-request limit, capped to bridge max (20)
|
|
1253
|
+
remaining = limit - len(comments)
|
|
1254
|
+
api_limit = min(20, remaining + 1) if remaining < 20 else 20
|
|
1253
1255
|
except Exception as e:
|
|
1254
1256
|
# If we get an error but have some posts, return what we have
|
|
1255
1257
|
if len(comments) > 0:
|
|
@@ -1312,9 +1314,9 @@ class AccountPosts(list):
|
|
|
1312
1314
|
if not self.blockchain.is_connected():
|
|
1313
1315
|
return None
|
|
1314
1316
|
comments = []
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1317
|
+
# Bridge API typically restricts page size to 20; cap per-request size
|
|
1318
|
+
# and page as needed until overall `limit` is satisfied.
|
|
1319
|
+
api_limit = min(limit, 20)
|
|
1318
1320
|
last_n = -1
|
|
1319
1321
|
while len(comments) < limit and last_n != len(comments):
|
|
1320
1322
|
last_n = len(comments)
|
|
@@ -1351,8 +1353,9 @@ class AccountPosts(list):
|
|
|
1351
1353
|
if len(comments) > 0:
|
|
1352
1354
|
start_author = comments[-1]["author"]
|
|
1353
1355
|
start_permlink = comments[-1]["permlink"]
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
+
# Recompute per-request limit for next page, still capped at 20
|
|
1357
|
+
remaining = limit - len(comments)
|
|
1358
|
+
api_limit = min(20, remaining + 1) if remaining < 20 else 20
|
|
1356
1359
|
except Exception as e:
|
|
1357
1360
|
# If we get an error but have some posts, return what we have
|
|
1358
1361
|
if len(comments) > 0:
|
nectar/haf.py
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from nectar.instance import shared_blockchain_instance
|
|
9
|
+
|
|
10
|
+
log = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HAF(object):
|
|
14
|
+
"""Hive Account Feed (HAF) API client for accessing Hive blockchain endpoints.
|
|
15
|
+
|
|
16
|
+
This class provides access to various Hive API endpoints that are not part of the
|
|
17
|
+
standard RPC blockchain calls. It supports multiple API providers and handles
|
|
18
|
+
reputation queries and other HAF-related data.
|
|
19
|
+
|
|
20
|
+
:param str api: Base API URL to use for requests. Supported endpoints include:
|
|
21
|
+
- 'https://api.hive.blog' (default)
|
|
22
|
+
- 'https://api.syncad.com'
|
|
23
|
+
:param blockchain_instance: Blockchain instance for compatibility with the nectar ecosystem
|
|
24
|
+
|
|
25
|
+
.. code-block:: python
|
|
26
|
+
|
|
27
|
+
>>> from nectar.haf import HAF
|
|
28
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
29
|
+
>>> reputation = haf.reputation("thecrazygm") # doctest: +SKIP
|
|
30
|
+
>>> print(reputation) # doctest: +SKIP
|
|
31
|
+
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
DEFAULT_APIS = ["https://api.hive.blog", "https://api.syncad.com"]
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, api: Optional[str] = None, blockchain_instance=None, timeout: Optional[float] = None
|
|
38
|
+
):
|
|
39
|
+
"""
|
|
40
|
+
Initialize the HAF client.
|
|
41
|
+
|
|
42
|
+
Parameters:
|
|
43
|
+
api (str, optional): Base API URL. If None, uses the first available default API.
|
|
44
|
+
blockchain_instance: Blockchain instance for ecosystem compatibility.
|
|
45
|
+
timeout (float, optional): Timeout for requests in seconds.
|
|
46
|
+
"""
|
|
47
|
+
self.api = api or self.DEFAULT_APIS[0]
|
|
48
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
49
|
+
self._timeout = float(timeout) if timeout else 30.0
|
|
50
|
+
|
|
51
|
+
# Validate API URL
|
|
52
|
+
if not self.api.startswith(("http://", "https://")):
|
|
53
|
+
raise ValueError(f"Invalid API URL: {self.api}. Must start with http:// or https://")
|
|
54
|
+
|
|
55
|
+
# Remove trailing slash if present
|
|
56
|
+
self.api = self.api.rstrip("/")
|
|
57
|
+
|
|
58
|
+
log.debug(f"Initialized HAF client with API: {self.api}")
|
|
59
|
+
|
|
60
|
+
def _make_request(self, endpoint: str, method: str = "GET", **kwargs) -> Any:
|
|
61
|
+
"""
|
|
62
|
+
Make an HTTP request to the HAF API.
|
|
63
|
+
|
|
64
|
+
Parameters:
|
|
65
|
+
endpoint (str): API endpoint path (without leading slash)
|
|
66
|
+
method (str): HTTP method (default: 'GET')
|
|
67
|
+
**kwargs: Additional arguments passed to requests
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
dict: JSON response from the API
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
requests.RequestException: If the request fails
|
|
74
|
+
ValueError: If the response is not valid JSON
|
|
75
|
+
"""
|
|
76
|
+
url = f"{self.api}/{endpoint}"
|
|
77
|
+
|
|
78
|
+
# Set default headers
|
|
79
|
+
headers = kwargs.pop("headers", {})
|
|
80
|
+
headers.setdefault("accept", "application/json")
|
|
81
|
+
headers.setdefault("User-Agent", "hive-nectar/0.1.3")
|
|
82
|
+
|
|
83
|
+
log.debug(f"Making {method} request to: {url}")
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
timeout = kwargs.pop("timeout", self._timeout)
|
|
87
|
+
response = requests.request(method, url, headers=headers, timeout=timeout, **kwargs)
|
|
88
|
+
response.raise_for_status()
|
|
89
|
+
|
|
90
|
+
return response.json()
|
|
91
|
+
|
|
92
|
+
except requests.RequestException as e:
|
|
93
|
+
log.error(f"Request failed for {url}: {e}")
|
|
94
|
+
raise
|
|
95
|
+
except json.JSONDecodeError as e:
|
|
96
|
+
log.error(f"Invalid JSON response from {url}: {e}")
|
|
97
|
+
raise ValueError(f"Invalid JSON response from API: {e}")
|
|
98
|
+
|
|
99
|
+
def reputation(self, account: str) -> Optional[Dict[str, Any]]:
|
|
100
|
+
"""
|
|
101
|
+
Get reputation information for a Hive account.
|
|
102
|
+
|
|
103
|
+
This method queries the reputation API endpoint to retrieve the account's
|
|
104
|
+
reputation score and related metadata.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
account (str): Hive account name
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
dict or None: Reputation data containing account information and reputation score,
|
|
111
|
+
or None if the request fails or account is not found
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
115
|
+
>>> rep = haf.reputation("thecrazygm") # doctest: +SKIP
|
|
116
|
+
>>> print(rep) # doctest: +SKIP
|
|
117
|
+
{'account': 'thecrazygm', 'reputation': '71', ...}
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
if not account or not isinstance(account, str):
|
|
121
|
+
raise ValueError("Account name must be a non-empty string")
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
endpoint = f"reputation-api/accounts/{account}/reputation"
|
|
125
|
+
response = self._make_request(endpoint)
|
|
126
|
+
|
|
127
|
+
log.debug(f"Retrieved reputation for account: {account}")
|
|
128
|
+
return response
|
|
129
|
+
|
|
130
|
+
except requests.RequestException as e:
|
|
131
|
+
log.warning(f"Failed to retrieve reputation for account {account}: {e}")
|
|
132
|
+
return None
|
|
133
|
+
except Exception as e:
|
|
134
|
+
log.error(f"Unexpected error retrieving reputation for {account}: {e}")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
def get_available_apis(self) -> list[str]:
|
|
138
|
+
"""
|
|
139
|
+
Get the list of available API endpoints.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
list: List of supported API URLs
|
|
143
|
+
"""
|
|
144
|
+
return self.DEFAULT_APIS.copy()
|
|
145
|
+
|
|
146
|
+
def set_api(self, api: str) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Change the API endpoint.
|
|
149
|
+
|
|
150
|
+
Parameters:
|
|
151
|
+
api (str): New API URL to use
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
ValueError: If the API URL is invalid
|
|
155
|
+
"""
|
|
156
|
+
if api not in self.DEFAULT_APIS:
|
|
157
|
+
log.warning(f"Using non-default API: {api}")
|
|
158
|
+
|
|
159
|
+
# Validate URL
|
|
160
|
+
if not api.startswith(("http://", "https://")):
|
|
161
|
+
raise ValueError(f"Invalid API URL: {api}. Must start with http:// or https://")
|
|
162
|
+
|
|
163
|
+
self.api = api.rstrip("/")
|
|
164
|
+
log.info(f"Switched to API: {self.api}")
|
|
165
|
+
|
|
166
|
+
def get_current_api(self) -> str:
|
|
167
|
+
"""
|
|
168
|
+
Get the currently active API endpoint.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
str: Current API URL
|
|
172
|
+
"""
|
|
173
|
+
return self.api
|
|
174
|
+
|
|
175
|
+
def get_account_balances(self, account: str) -> Optional[Dict[str, Any]]:
|
|
176
|
+
"""
|
|
177
|
+
Get account balances from the balance API.
|
|
178
|
+
|
|
179
|
+
This method retrieves comprehensive balance information including HBD, HIVE,
|
|
180
|
+
vesting shares, rewards, and other balance-related data for an account.
|
|
181
|
+
|
|
182
|
+
Parameters:
|
|
183
|
+
account (str): Hive account name
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
dict or None: Account balance data or None if request fails
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
190
|
+
>>> balances = haf.get_account_balances("thecrazygm") # doctest: +SKIP
|
|
191
|
+
>>> print(balances['hive_balance']) # doctest: +SKIP
|
|
192
|
+
"""
|
|
193
|
+
if not account or not isinstance(account, str):
|
|
194
|
+
raise ValueError("Account name must be a non-empty string")
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
endpoint = f"balance-api/accounts/{account}/balances"
|
|
198
|
+
response = self._make_request(endpoint)
|
|
199
|
+
|
|
200
|
+
log.debug(f"Retrieved balances for account: {account}")
|
|
201
|
+
return response
|
|
202
|
+
|
|
203
|
+
except requests.RequestException as e:
|
|
204
|
+
log.warning(f"Failed to retrieve balances for account {account}: {e}")
|
|
205
|
+
return None
|
|
206
|
+
except Exception as e:
|
|
207
|
+
log.error(f"Unexpected error retrieving balances for {account}: {e}")
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
def get_account_delegations(self, account: str) -> Optional[Dict[str, Any]]:
|
|
211
|
+
"""
|
|
212
|
+
Get account delegations from the balance API.
|
|
213
|
+
|
|
214
|
+
This method retrieves both incoming and outgoing delegations for an account.
|
|
215
|
+
|
|
216
|
+
Parameters:
|
|
217
|
+
account (str): Hive account name
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
dict or None: Delegation data containing incoming and outgoing delegations
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
224
|
+
>>> delegations = haf.get_account_delegations("thecrazygm") # doctest: +SKIP
|
|
225
|
+
>>> print(delegations['incoming_delegations']) # doctest: +SKIP
|
|
226
|
+
"""
|
|
227
|
+
if not account or not isinstance(account, str):
|
|
228
|
+
raise ValueError("Account name must be a non-empty string")
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
endpoint = f"balance-api/accounts/{account}/delegations"
|
|
232
|
+
response = self._make_request(endpoint)
|
|
233
|
+
|
|
234
|
+
log.debug(f"Retrieved delegations for account: {account}")
|
|
235
|
+
return response
|
|
236
|
+
|
|
237
|
+
except requests.RequestException as e:
|
|
238
|
+
log.warning(f"Failed to retrieve delegations for account {account}: {e}")
|
|
239
|
+
return None
|
|
240
|
+
except Exception as e:
|
|
241
|
+
log.error(f"Unexpected error retrieving delegations for {account}: {e}")
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def get_account_recurrent_transfers(self, account: str) -> Optional[Dict[str, Any]]:
|
|
245
|
+
"""
|
|
246
|
+
Get account recurrent transfers from the balance API.
|
|
247
|
+
|
|
248
|
+
This method retrieves both incoming and outgoing recurrent transfers for an account.
|
|
249
|
+
|
|
250
|
+
Parameters:
|
|
251
|
+
account (str): Hive account name
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
dict or None: Recurrent transfer data containing incoming and outgoing transfers
|
|
255
|
+
|
|
256
|
+
Example:
|
|
257
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
258
|
+
>>> transfers = haf.get_account_recurrent_transfers("thecrazygm") # doctest: +SKIP
|
|
259
|
+
>>> print(transfers['outgoing_recurrent_transfers']) # doctest: +SKIP
|
|
260
|
+
"""
|
|
261
|
+
if not account or not isinstance(account, str):
|
|
262
|
+
raise ValueError("Account name must be a non-empty string")
|
|
263
|
+
|
|
264
|
+
try:
|
|
265
|
+
endpoint = f"balance-api/accounts/{account}/recurrent-transfers"
|
|
266
|
+
response = self._make_request(endpoint)
|
|
267
|
+
|
|
268
|
+
log.debug(f"Retrieved recurrent transfers for account: {account}")
|
|
269
|
+
return response
|
|
270
|
+
|
|
271
|
+
except requests.RequestException as e:
|
|
272
|
+
log.warning(f"Failed to retrieve recurrent transfers for account {account}: {e}")
|
|
273
|
+
return None
|
|
274
|
+
except Exception as e:
|
|
275
|
+
log.error(f"Unexpected error retrieving recurrent transfers for {account}: {e}")
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
def get_reputation_version(self) -> Optional[str]:
|
|
279
|
+
"""
|
|
280
|
+
Get the reputation tracker's version from the reputation API.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
str or None: Version string or None if request fails
|
|
284
|
+
|
|
285
|
+
Example:
|
|
286
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
287
|
+
>>> version = haf.get_reputation_version() # doctest: +SKIP
|
|
288
|
+
>>> print(version) # doctest: +SKIP
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
endpoint = "reputation-api/version"
|
|
292
|
+
response = self._make_request(endpoint)
|
|
293
|
+
|
|
294
|
+
log.debug("Retrieved reputation tracker version")
|
|
295
|
+
return response
|
|
296
|
+
|
|
297
|
+
except requests.RequestException as e:
|
|
298
|
+
log.warning(f"Failed to retrieve reputation version: {e}")
|
|
299
|
+
return None
|
|
300
|
+
except Exception as e:
|
|
301
|
+
log.error(f"Unexpected error retrieving reputation version: {e}")
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
def get_reputation_last_synced_block(self) -> Optional[int]:
|
|
305
|
+
"""
|
|
306
|
+
Get the last block number synced by the reputation tracker.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
int or None: Last synced block number or None if request fails
|
|
310
|
+
|
|
311
|
+
Example:
|
|
312
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
313
|
+
>>> block = haf.get_reputation_last_synced_block() # doctest: +SKIP
|
|
314
|
+
>>> print(block) # doctest: +SKIP
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
endpoint = "reputation-api/last-synced-block"
|
|
318
|
+
response = self._make_request(endpoint)
|
|
319
|
+
|
|
320
|
+
log.debug("Retrieved last synced block for reputation tracker")
|
|
321
|
+
return response
|
|
322
|
+
|
|
323
|
+
except requests.RequestException as e:
|
|
324
|
+
log.warning(f"Failed to retrieve last synced block: {e}")
|
|
325
|
+
return None
|
|
326
|
+
except Exception as e:
|
|
327
|
+
log.error(f"Unexpected error retrieving last synced block: {e}")
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
def get_balance_version(self) -> Optional[str]:
|
|
331
|
+
"""
|
|
332
|
+
Get the balance tracker's version from the balance API.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
str or None: Version string or None if request fails
|
|
336
|
+
|
|
337
|
+
Example:
|
|
338
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
339
|
+
>>> version = haf.get_balance_version() # doctest: +SKIP
|
|
340
|
+
>>> print(version) # doctest: +SKIP
|
|
341
|
+
"""
|
|
342
|
+
try:
|
|
343
|
+
endpoint = "balance-api/version"
|
|
344
|
+
response = self._make_request(endpoint)
|
|
345
|
+
|
|
346
|
+
log.debug("Retrieved balance tracker version")
|
|
347
|
+
return response
|
|
348
|
+
|
|
349
|
+
except requests.RequestException as e:
|
|
350
|
+
log.warning(f"Failed to retrieve balance version: {e}")
|
|
351
|
+
return None
|
|
352
|
+
except Exception as e:
|
|
353
|
+
log.error(f"Unexpected error retrieving balance version: {e}")
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
def get_balance_last_synced_block(self) -> Optional[int]:
|
|
357
|
+
"""
|
|
358
|
+
Get the last block number synced by the balance tracker.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
int or None: Last synced block number or None if request fails
|
|
362
|
+
|
|
363
|
+
Example:
|
|
364
|
+
>>> haf = HAF() # doctest: +SKIP
|
|
365
|
+
>>> block = haf.get_balance_last_synced_block() # doctest: +SKIP
|
|
366
|
+
>>> print(block) # doctest: +SKIP
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
endpoint = "balance-api/last-synced-block"
|
|
370
|
+
response = self._make_request(endpoint)
|
|
371
|
+
|
|
372
|
+
log.debug("Retrieved last synced block for balance tracker")
|
|
373
|
+
return response
|
|
374
|
+
|
|
375
|
+
except requests.RequestException as e:
|
|
376
|
+
log.warning(f"Failed to retrieve last synced block: {e}")
|
|
377
|
+
return None
|
|
378
|
+
except Exception as e:
|
|
379
|
+
log.error(f"Unexpected error retrieving last synced block: {e}")
|
|
380
|
+
return None
|
nectar/hivesigner.py
CHANGED
|
@@ -374,6 +374,15 @@ class HiveSigner(object):
|
|
|
374
374
|
|
|
375
375
|
return r.json()
|
|
376
376
|
|
|
377
|
+
def create_hot_sign_url(self, operation, params, redirect_uri=None):
|
|
378
|
+
url = urljoin(self.hs_oauth_base_url, "sign/" + operation)
|
|
379
|
+
if redirect_uri:
|
|
380
|
+
params["redirect_uri"] = redirect_uri
|
|
381
|
+
if PY2:
|
|
382
|
+
return url + "?" + urlencode(params).replace("%2C", ",")
|
|
383
|
+
else:
|
|
384
|
+
return url + "?" + urlencode(params, safe=",")
|
|
385
|
+
|
|
377
386
|
def url_from_tx(self, tx, redirect_uri=None):
|
|
378
387
|
"""
|
|
379
388
|
Generate HiveSigner hot-sign URLs for each operation in a transaction.
|
nectar/market.py
CHANGED
|
@@ -330,9 +330,8 @@ class Market(dict):
|
|
|
330
330
|
:param int limit: Defines how many trades are fetched at each intervall point
|
|
331
331
|
:param bool raw_data: when True, the raw data are returned
|
|
332
332
|
"""
|
|
333
|
-
utc = timezone.utc
|
|
334
333
|
if not stop:
|
|
335
|
-
stop =
|
|
334
|
+
stop = datetime.now(timezone.utc)
|
|
336
335
|
if not start:
|
|
337
336
|
start = stop - timedelta(hours=1)
|
|
338
337
|
start = addTzInfo(start)
|
|
@@ -370,9 +369,8 @@ class Market(dict):
|
|
|
370
369
|
"""
|
|
371
370
|
# FIXME, this call should also return whether it was a buy or
|
|
372
371
|
# sell
|
|
373
|
-
utc = timezone.utc
|
|
374
372
|
if not stop:
|
|
375
|
-
stop =
|
|
373
|
+
stop = datetime.now(timezone.utc)
|
|
376
374
|
if not start:
|
|
377
375
|
start = stop - timedelta(hours=24)
|
|
378
376
|
start = addTzInfo(start)
|
nectar/memo.py
CHANGED
|
@@ -262,7 +262,7 @@ class Memo(object):
|
|
|
262
262
|
)
|
|
263
263
|
enc = unhexlify(base58decode(enc[1:]))
|
|
264
264
|
shared_secret = BtsMemo.get_shared_secret(priv, pub)
|
|
265
|
-
aes, check = BtsMemo.
|
|
265
|
+
aes, check = BtsMemo.init_aes2(shared_secret, nonce)
|
|
266
266
|
with open(outfile, "wb") as fout:
|
|
267
267
|
fout.write(struct.pack("<Q", len(enc)))
|
|
268
268
|
fout.write(enc)
|
|
@@ -449,7 +449,7 @@ class Memo(object):
|
|
|
449
449
|
nectar_version = BtsMemo.decode_memo(priv, memo)
|
|
450
450
|
shared_secret = BtsMemo.get_shared_secret(priv, pubkey)
|
|
451
451
|
# Init encryption
|
|
452
|
-
aes, checksum = BtsMemo.
|
|
452
|
+
aes, checksum = BtsMemo.init_aes2(shared_secret, nonce)
|
|
453
453
|
with open(infile, "rb") as fin:
|
|
454
454
|
memo_size = struct.unpack("<Q", fin.read(struct.calcsize("<Q")))[0]
|
|
455
455
|
memo = fin.read(memo_size)
|
nectar/nodelist.py
CHANGED
|
@@ -130,22 +130,6 @@ class NodeList(list):
|
|
|
130
130
|
"hive": True,
|
|
131
131
|
"score": 40,
|
|
132
132
|
},
|
|
133
|
-
{
|
|
134
|
-
"url": "https://hive-api.dlux.io",
|
|
135
|
-
"version": "1.27.8",
|
|
136
|
-
"type": "appbase",
|
|
137
|
-
"owner": "dlux",
|
|
138
|
-
"hive": True,
|
|
139
|
-
"score": 30,
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
"url": "https://api.hive.blue",
|
|
143
|
-
"version": "1.27.5",
|
|
144
|
-
"type": "appbase",
|
|
145
|
-
"owner": "hive.blue",
|
|
146
|
-
"hive": True,
|
|
147
|
-
"score": 30,
|
|
148
|
-
},
|
|
149
133
|
{
|
|
150
134
|
"url": "https://hiveapi.actifit.io",
|
|
151
135
|
"version": "1.27.8",
|
|
@@ -162,14 +146,6 @@ class NodeList(list):
|
|
|
162
146
|
"hive": True,
|
|
163
147
|
"score": 20,
|
|
164
148
|
},
|
|
165
|
-
{
|
|
166
|
-
"url": "https://hive-test-beeabode.roelandp.nl",
|
|
167
|
-
"version": "0.23.0",
|
|
168
|
-
"type": "testnet",
|
|
169
|
-
"owner": "roelandp",
|
|
170
|
-
"hive": True,
|
|
171
|
-
"score": 5,
|
|
172
|
-
},
|
|
173
149
|
]
|
|
174
150
|
super(NodeList, self).__init__(nodes)
|
|
175
151
|
|
nectar/utils.py
CHANGED
|
@@ -318,44 +318,67 @@ def findall_patch_hunks(body=None):
|
|
|
318
318
|
|
|
319
319
|
|
|
320
320
|
def derive_beneficiaries(beneficiaries):
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
321
|
+
"""
|
|
322
|
+
Parse beneficiaries and return a normalized, merged list of unique accounts with weights in basis points.
|
|
323
|
+
|
|
324
|
+
Accepts a comma-separated string or list with items like "account:10", "@account:10%", or "account" (unknown
|
|
325
|
+
percentage). Duplicate accounts are merged by summing their explicit percentages and any share of the remaining
|
|
326
|
+
percentage allocated to unknown entries. Unknown entries are distributed equally across all unknown slots.
|
|
327
|
+
|
|
328
|
+
Returns a list of dicts sorted by account name: [{"account": str, "weight": int_basis_points}]
|
|
329
|
+
where weight is expressed in basis points (e.g., 1000 == 10%).
|
|
330
|
+
"""
|
|
331
|
+
# Normalize input to list of entries
|
|
332
|
+
entries = beneficiaries if isinstance(beneficiaries, list) else beneficiaries.split(",")
|
|
333
|
+
|
|
334
|
+
# Collect known percentages and unknown slots per account
|
|
335
|
+
accounts = {}
|
|
336
|
+
total_known_bp = 0 # basis points (1% == 100)
|
|
337
|
+
total_unknown_slots = 0
|
|
338
|
+
|
|
339
|
+
for raw in entries:
|
|
340
|
+
token = raw.strip()
|
|
341
|
+
if not token:
|
|
332
342
|
continue
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
343
|
+
name_part = token.split(":")[0].strip()
|
|
344
|
+
account = name_part[1:] if name_part.startswith("@") else name_part
|
|
345
|
+
if account not in accounts:
|
|
346
|
+
accounts[account] = {"known_bp": 0, "unknown_slots": 0}
|
|
347
|
+
|
|
348
|
+
if ":" not in token:
|
|
349
|
+
# Unknown slot for this account
|
|
350
|
+
accounts[account]["unknown_slots"] += 1
|
|
351
|
+
total_unknown_slots += 1
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
# Parse percentage
|
|
355
|
+
perc_str = token.split(":", 1)[1].strip()
|
|
356
|
+
if perc_str.endswith("%"):
|
|
357
|
+
perc_str = perc_str[:-1].strip()
|
|
358
|
+
try:
|
|
359
|
+
perc = float(perc_str)
|
|
360
|
+
except Exception:
|
|
361
|
+
# Treat unparsable as unknown slot
|
|
362
|
+
accounts[account]["unknown_slots"] += 1
|
|
363
|
+
total_unknown_slots += 1
|
|
364
|
+
continue
|
|
365
|
+
bp = int(perc * 100)
|
|
366
|
+
accounts[account]["known_bp"] += bp
|
|
367
|
+
total_known_bp += bp
|
|
368
|
+
|
|
369
|
+
# Distribute remaining to unknown slots equally (in bp)
|
|
370
|
+
remaining_bp = max(0, 10000 - total_known_bp)
|
|
371
|
+
if total_unknown_slots > 0 and remaining_bp > 0:
|
|
372
|
+
for account, data in accounts.items():
|
|
373
|
+
slots = data["unknown_slots"]
|
|
374
|
+
if slots > 0:
|
|
375
|
+
share_bp = int((remaining_bp * slots) / total_unknown_slots)
|
|
376
|
+
data["known_bp"] += share_bp
|
|
377
|
+
|
|
378
|
+
# Build final list (unique accounts) and sort deterministically
|
|
379
|
+
result = [{"account": acc, "weight": data["known_bp"]} for acc, data in accounts.items()]
|
|
380
|
+
result.sort(key=lambda x: x["account"])
|
|
381
|
+
return result
|
|
359
382
|
|
|
360
383
|
|
|
361
384
|
def derive_tags(tags):
|
nectar/version.py
CHANGED