hive-nectar 0.2.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hive_nectar-0.2.9.dist-info/METADATA +194 -0
- hive_nectar-0.2.9.dist-info/RECORD +87 -0
- hive_nectar-0.2.9.dist-info/WHEEL +4 -0
- hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
- hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +37 -0
- nectar/account.py +5076 -0
- nectar/amount.py +553 -0
- nectar/asciichart.py +303 -0
- nectar/asset.py +122 -0
- nectar/block.py +574 -0
- nectar/blockchain.py +1242 -0
- nectar/blockchaininstance.py +2590 -0
- nectar/blockchainobject.py +263 -0
- nectar/cli.py +5937 -0
- nectar/comment.py +1552 -0
- nectar/community.py +854 -0
- nectar/constants.py +95 -0
- nectar/discussions.py +1437 -0
- nectar/exceptions.py +152 -0
- nectar/haf.py +381 -0
- nectar/hive.py +630 -0
- nectar/imageuploader.py +114 -0
- nectar/instance.py +113 -0
- nectar/market.py +876 -0
- nectar/memo.py +542 -0
- nectar/message.py +379 -0
- nectar/nodelist.py +309 -0
- nectar/price.py +603 -0
- nectar/profile.py +74 -0
- nectar/py.typed +0 -0
- nectar/rc.py +333 -0
- nectar/snapshot.py +1024 -0
- nectar/storage.py +62 -0
- nectar/transactionbuilder.py +659 -0
- nectar/utils.py +630 -0
- nectar/version.py +3 -0
- nectar/vote.py +722 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +728 -0
- nectarapi/__init__.py +12 -0
- nectarapi/exceptions.py +126 -0
- nectarapi/graphenerpc.py +596 -0
- nectarapi/node.py +194 -0
- nectarapi/noderpc.py +79 -0
- nectarapi/openapi.py +107 -0
- nectarapi/py.typed +0 -0
- nectarapi/rpcutils.py +98 -0
- nectarapi/version.py +3 -0
- nectarbase/__init__.py +15 -0
- nectarbase/ledgertransactions.py +106 -0
- nectarbase/memo.py +242 -0
- nectarbase/objects.py +521 -0
- nectarbase/objecttypes.py +21 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1357 -0
- nectarbase/py.typed +0 -0
- nectarbase/signedtransactions.py +89 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +3 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +1121 -0
- nectargraphenebase/aes.py +49 -0
- nectargraphenebase/base58.py +197 -0
- nectargraphenebase/bip32.py +575 -0
- nectargraphenebase/bip38.py +110 -0
- nectargraphenebase/chains.py +15 -0
- nectargraphenebase/dictionary.py +2 -0
- nectargraphenebase/ecdsasig.py +309 -0
- nectargraphenebase/objects.py +130 -0
- nectargraphenebase/objecttypes.py +8 -0
- nectargraphenebase/operationids.py +5 -0
- nectargraphenebase/operations.py +25 -0
- nectargraphenebase/prefix.py +13 -0
- nectargraphenebase/py.typed +0 -0
- nectargraphenebase/signedtransactions.py +221 -0
- nectargraphenebase/types.py +557 -0
- nectargraphenebase/unsignedtransactions.py +288 -0
- nectargraphenebase/version.py +3 -0
- nectarstorage/__init__.py +57 -0
- nectarstorage/base.py +317 -0
- nectarstorage/exceptions.py +15 -0
- nectarstorage/interfaces.py +244 -0
- nectarstorage/masterpassword.py +237 -0
- nectarstorage/py.typed +0 -0
- nectarstorage/ram.py +27 -0
- nectarstorage/sqlite.py +343 -0
nectar/comment.py
ADDED
|
@@ -0,0 +1,1552 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import math
|
|
4
|
+
from datetime import date, datetime, timedelta, timezone
|
|
5
|
+
from typing import Any, Dict, Optional, Union
|
|
6
|
+
|
|
7
|
+
from nectar.constants import (
|
|
8
|
+
HIVE_100_PERCENT,
|
|
9
|
+
HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF6,
|
|
10
|
+
HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF20,
|
|
11
|
+
HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF21,
|
|
12
|
+
)
|
|
13
|
+
from nectarbase import operations
|
|
14
|
+
|
|
15
|
+
from .account import Account
|
|
16
|
+
from .amount import Amount
|
|
17
|
+
from .blockchainobject import BlockchainObject
|
|
18
|
+
from .exceptions import ContentDoesNotExistsException, VotingInvalidOnArchivedPost
|
|
19
|
+
from .instance import shared_blockchain_instance
|
|
20
|
+
from .price import Price
|
|
21
|
+
from .utils import (
|
|
22
|
+
construct_authorperm,
|
|
23
|
+
formatTimeString,
|
|
24
|
+
formatToTimeStamp,
|
|
25
|
+
make_patch,
|
|
26
|
+
parse_time,
|
|
27
|
+
resolve_authorperm,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
log = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Comment(BlockchainObject):
|
|
34
|
+
"""Read data about a Comment/Post in the chain
|
|
35
|
+
|
|
36
|
+
:param str authorperm: identifier to post/comment in the form of
|
|
37
|
+
``@author/permlink``
|
|
38
|
+
:param str tags: defines which api is used. Can be bridge, tags, condenser or database (default = bridge)
|
|
39
|
+
:param Blockchain blockchain_instance: Blockchain instance to use when accessing the RPC
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
.. code-block:: python
|
|
43
|
+
|
|
44
|
+
>>> from nectar.comment import Comment
|
|
45
|
+
>>> from nectar.account import Account
|
|
46
|
+
>>> # Create a Hive blockchain instance
|
|
47
|
+
>>> from nectar import Hive
|
|
48
|
+
>>> hv = Hive()
|
|
49
|
+
>>> acc = Account("gtg", blockchain_instance=hv)
|
|
50
|
+
>>> authorperm = acc.get_blog(limit=1)[0]["authorperm"]
|
|
51
|
+
>>> c = Comment(authorperm)
|
|
52
|
+
>>> postdate = c["created"]
|
|
53
|
+
>>> postdate_str = c.json()["created"]
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
type_id = 8
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
authorperm: Union[str, Dict[str, Any]],
|
|
62
|
+
api: str = "bridge",
|
|
63
|
+
observer: str = "",
|
|
64
|
+
full: bool = True,
|
|
65
|
+
lazy: bool = False,
|
|
66
|
+
blockchain_instance: Optional[Any] = None,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Create a Comment object representing a Hive post or comment.
|
|
70
|
+
|
|
71
|
+
Supports initializing from either an author/permlink string ("author/permlink") or a dict containing at least "author" and "permlink". For a string input the constructor resolves and stores author, permlink, and authorperm. For a dict input the constructor normalizes the dict via _parse_json_data (timestamps, amounts, metadata) and sets the canonical "authorperm" before delegating to the BlockchainObject constructor.
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
authorperm: Either an "author/permlink" string or a dict with "author" and "permlink".
|
|
75
|
+
api: RPC bridge to use (defaults to "bridge"); stored on the instance.
|
|
76
|
+
observer: Optional observer identifier stored on the instance.
|
|
77
|
+
full: If True, load all fields immediately; if False, allow partial/lazy loading.
|
|
78
|
+
lazy: If True, delay full object loading until needed.
|
|
79
|
+
|
|
80
|
+
Note: The blockchain instance is taken from blockchain_instance (if provided) or the module's shared_blockchain_instance(). The constructor sets instance attributes and then calls the parent initializer with id_item="authorperm".
|
|
81
|
+
"""
|
|
82
|
+
self.full = full
|
|
83
|
+
self.lazy = lazy
|
|
84
|
+
self.api = api
|
|
85
|
+
self.observer = observer
|
|
86
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
87
|
+
if isinstance(authorperm, str) and authorperm != "":
|
|
88
|
+
[author, permlink] = resolve_authorperm(authorperm)
|
|
89
|
+
self["id"] = 0
|
|
90
|
+
self["author"] = author
|
|
91
|
+
self["permlink"] = permlink
|
|
92
|
+
self["authorperm"] = authorperm
|
|
93
|
+
elif isinstance(authorperm, dict) and "author" in authorperm and "permlink" in authorperm:
|
|
94
|
+
authorperm["authorperm"] = construct_authorperm(
|
|
95
|
+
authorperm["author"], authorperm["permlink"]
|
|
96
|
+
)
|
|
97
|
+
authorperm = self._parse_json_data(authorperm)
|
|
98
|
+
super().__init__(
|
|
99
|
+
authorperm,
|
|
100
|
+
id_item="authorperm",
|
|
101
|
+
lazy=lazy,
|
|
102
|
+
full=full,
|
|
103
|
+
blockchain_instance=self.blockchain,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def _parse_json_data(self, comment: Dict[str, Any]) -> Dict[str, Any]:
|
|
107
|
+
"""
|
|
108
|
+
Normalize and convert raw comment JSON fields into Python-native types.
|
|
109
|
+
|
|
110
|
+
This parses and mutates the given comment dict in-place and returns it. Normalizations:
|
|
111
|
+
- Converts known timestamp strings (e.g., "created", "last_update", "cashout_time") to datetime using formatTimeString.
|
|
112
|
+
- Converts monetary fields backed by the chain's backed token (HBD) into Amount objects using the instance's backed_token_symbol.
|
|
113
|
+
- Ensures a "community" key exists and parses `json_metadata` (string/bytes) into a dict; extracts `tags` and `community` from that metadata when present.
|
|
114
|
+
- Converts numeric string fields like `author_reputation` and `net_rshares` to ints.
|
|
115
|
+
- Normalizes each entry in `active_votes`: converts vote `time` to datetime and numeric strings (`rshares`, `reputation`) to ints (falling back to 0 on parse errors).
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
comment (dict): Raw comment/post data as returned by the node RPC.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
dict: The same comment dict with normalized fields (timestamps as datetimes, amounts as Amount objects, json_metadata as dict, numeric fields as ints).
|
|
122
|
+
"""
|
|
123
|
+
parse_times = [
|
|
124
|
+
"active",
|
|
125
|
+
"cashout_time",
|
|
126
|
+
"created",
|
|
127
|
+
"last_payout",
|
|
128
|
+
"last_update",
|
|
129
|
+
"updated",
|
|
130
|
+
"max_cashout_time",
|
|
131
|
+
]
|
|
132
|
+
for p in parse_times:
|
|
133
|
+
if p in comment and isinstance(comment.get(p), str):
|
|
134
|
+
comment[p] = parse_time(comment.get(p, "1970-01-01T00:00:00"))
|
|
135
|
+
if "parent_author" not in comment:
|
|
136
|
+
comment["parent_author"] = ""
|
|
137
|
+
if "parent_permlink" not in comment:
|
|
138
|
+
comment["parent_permlink"] = ""
|
|
139
|
+
# Parse Amounts
|
|
140
|
+
hbd_amounts = [
|
|
141
|
+
"total_payout_value",
|
|
142
|
+
"max_accepted_payout",
|
|
143
|
+
"pending_payout_value",
|
|
144
|
+
"curator_payout_value",
|
|
145
|
+
"total_pending_payout_value",
|
|
146
|
+
"promoted",
|
|
147
|
+
]
|
|
148
|
+
for p in hbd_amounts:
|
|
149
|
+
if p in comment and isinstance(comment.get(p), (str, list, dict)):
|
|
150
|
+
value = comment.get(p, "0.000 %s" % (self.blockchain.backed_token_symbol))
|
|
151
|
+
if (
|
|
152
|
+
isinstance(value, str)
|
|
153
|
+
and value.split(" ")[1] != self.blockchain.backed_token_symbol
|
|
154
|
+
):
|
|
155
|
+
value = value.split(" ")[0] + " " + self.blockchain.backed_token_symbol
|
|
156
|
+
comment[p] = Amount(value, blockchain_instance=self.blockchain)
|
|
157
|
+
|
|
158
|
+
if "community" not in comment:
|
|
159
|
+
comment["community"] = ""
|
|
160
|
+
|
|
161
|
+
# turn json_metadata into python dict
|
|
162
|
+
meta_str = comment.get("json_metadata", "{}")
|
|
163
|
+
if meta_str == "{}":
|
|
164
|
+
comment["json_metadata"] = meta_str
|
|
165
|
+
if isinstance(meta_str, (str, bytes, bytearray)):
|
|
166
|
+
try:
|
|
167
|
+
comment["json_metadata"] = json.loads(meta_str)
|
|
168
|
+
except Exception:
|
|
169
|
+
comment["json_metadata"] = {}
|
|
170
|
+
|
|
171
|
+
comment["tags"] = []
|
|
172
|
+
if isinstance(comment["json_metadata"], dict):
|
|
173
|
+
if "tags" in comment["json_metadata"]:
|
|
174
|
+
comment["tags"] = comment["json_metadata"]["tags"]
|
|
175
|
+
if "community" in comment["json_metadata"]:
|
|
176
|
+
comment["community"] = comment["json_metadata"]["community"]
|
|
177
|
+
|
|
178
|
+
parse_int = [
|
|
179
|
+
"author_reputation",
|
|
180
|
+
"net_rshares",
|
|
181
|
+
]
|
|
182
|
+
for p in parse_int:
|
|
183
|
+
if p in comment and isinstance(comment.get(p), str):
|
|
184
|
+
comment[p] = int(comment.get(p, "0"))
|
|
185
|
+
|
|
186
|
+
if "active_votes" in comment:
|
|
187
|
+
new_active_votes = []
|
|
188
|
+
for vote in comment["active_votes"]:
|
|
189
|
+
if "time" in vote and isinstance(vote.get("time"), str):
|
|
190
|
+
vote["time"] = parse_time(vote.get("time", "1970-01-01T00:00:00"))
|
|
191
|
+
parse_int = [
|
|
192
|
+
"rshares",
|
|
193
|
+
"reputation",
|
|
194
|
+
]
|
|
195
|
+
for p in parse_int:
|
|
196
|
+
if p in vote and isinstance(vote.get(p), str):
|
|
197
|
+
try:
|
|
198
|
+
vote[p] = int(vote.get(p, "0"))
|
|
199
|
+
except ValueError:
|
|
200
|
+
vote[p] = int(0)
|
|
201
|
+
new_active_votes.append(vote)
|
|
202
|
+
comment["active_votes"] = new_active_votes
|
|
203
|
+
return comment
|
|
204
|
+
|
|
205
|
+
def refresh(self) -> None:
|
|
206
|
+
if not self.identifier:
|
|
207
|
+
return
|
|
208
|
+
if not self.blockchain.is_connected():
|
|
209
|
+
return
|
|
210
|
+
[author, permlink] = resolve_authorperm(str(self.identifier))
|
|
211
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
212
|
+
from nectarapi.exceptions import InvalidParameters
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
if self.api == "condenser_api":
|
|
216
|
+
content = self.blockchain.rpc.get_content(
|
|
217
|
+
author,
|
|
218
|
+
permlink,
|
|
219
|
+
api="condenser_api",
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
# Default to bridge.get_post; database_api does not expose get_post
|
|
223
|
+
content = self.blockchain.rpc.get_post(
|
|
224
|
+
{"author": author, "permlink": permlink, "observer": self.observer},
|
|
225
|
+
api="bridge",
|
|
226
|
+
)
|
|
227
|
+
if content is not None and "comments" in content:
|
|
228
|
+
content = content["comments"]
|
|
229
|
+
if isinstance(content, list) and len(content) > 0:
|
|
230
|
+
content = content[0]
|
|
231
|
+
except InvalidParameters:
|
|
232
|
+
raise ContentDoesNotExistsException(self.identifier)
|
|
233
|
+
if not content or not content["author"] or not content["permlink"]:
|
|
234
|
+
raise ContentDoesNotExistsException(self.identifier)
|
|
235
|
+
content = self._parse_json_data(content)
|
|
236
|
+
content["authorperm"] = construct_authorperm(content["author"], content["permlink"])
|
|
237
|
+
super().__init__(
|
|
238
|
+
content,
|
|
239
|
+
id_item="authorperm",
|
|
240
|
+
lazy=self.lazy,
|
|
241
|
+
full=self.full,
|
|
242
|
+
blockchain_instance=self.blockchain,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
def json(self) -> Dict[str, Any]:
|
|
246
|
+
"""
|
|
247
|
+
Return a JSON-serializable dict representation of the Comment.
|
|
248
|
+
|
|
249
|
+
Removes internal-only keys (e.g., "authorperm", "tags"), ensures json-compatible types, and normalizes several fields so the result can be safely serialized to JSON and consumed by external callers or APIs. Normalizations performed:
|
|
250
|
+
- Serializes `json_metadata` to a compact JSON string.
|
|
251
|
+
- Converts datetime/date values in fields like "created", "updated", "last_payout", "cashout_time", "active", and "max_cashout_time" to formatted time strings.
|
|
252
|
+
- Converts Amount instances in HBD-related fields (e.g., "total_payout_value", "pending_payout_value", "curator_payout_value", "promoted", etc.) to their JSON representation via Amount.json().
|
|
253
|
+
- Converts selected integer fields ("author_reputation", "net_rshares") and vote numeric fields ("rshares", "reputation") to strings to preserve precision across transports.
|
|
254
|
+
- Normalizes times and numeric fields inside each entry of "active_votes".
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
dict: A JSON-safe copy of the comment data suitable for json.dumps or returning from an API.
|
|
258
|
+
"""
|
|
259
|
+
output = self.copy()
|
|
260
|
+
if "authorperm" in output:
|
|
261
|
+
output.pop("authorperm")
|
|
262
|
+
if "json_metadata" in output:
|
|
263
|
+
output["json_metadata"] = json.dumps(output["json_metadata"])
|
|
264
|
+
if "tags" in output:
|
|
265
|
+
output.pop("tags")
|
|
266
|
+
parse_times = [
|
|
267
|
+
"active",
|
|
268
|
+
"cashout_time",
|
|
269
|
+
"created",
|
|
270
|
+
"last_payout",
|
|
271
|
+
"last_update",
|
|
272
|
+
"updated",
|
|
273
|
+
"max_cashout_time",
|
|
274
|
+
]
|
|
275
|
+
for p in parse_times:
|
|
276
|
+
if p in output:
|
|
277
|
+
p_date = output.get(p, datetime(1970, 1, 1, 0, 0))
|
|
278
|
+
if isinstance(p_date, (datetime, date)):
|
|
279
|
+
output[p] = formatTimeString(p_date)
|
|
280
|
+
else:
|
|
281
|
+
output[p] = p_date
|
|
282
|
+
hbd_amounts = [
|
|
283
|
+
"total_payout_value",
|
|
284
|
+
"max_accepted_payout",
|
|
285
|
+
"pending_payout_value",
|
|
286
|
+
"curator_payout_value",
|
|
287
|
+
"total_pending_payout_value",
|
|
288
|
+
"promoted",
|
|
289
|
+
]
|
|
290
|
+
for p in hbd_amounts:
|
|
291
|
+
if p in output and isinstance(output[p], Amount):
|
|
292
|
+
output[p] = output[p].json()
|
|
293
|
+
parse_int = [
|
|
294
|
+
"author_reputation",
|
|
295
|
+
"net_rshares",
|
|
296
|
+
]
|
|
297
|
+
for p in parse_int:
|
|
298
|
+
if p in output and isinstance(output[p], int):
|
|
299
|
+
output[p] = str(output[p])
|
|
300
|
+
if "active_votes" in output:
|
|
301
|
+
new_active_votes = []
|
|
302
|
+
for vote in output["active_votes"]:
|
|
303
|
+
if "time" in vote:
|
|
304
|
+
p_date = vote.get("time", datetime(1970, 1, 1, 0, 0))
|
|
305
|
+
if isinstance(p_date, (datetime, date)):
|
|
306
|
+
vote["time"] = formatTimeString(p_date)
|
|
307
|
+
else:
|
|
308
|
+
vote["time"] = p_date
|
|
309
|
+
parse_int = [
|
|
310
|
+
"rshares",
|
|
311
|
+
"reputation",
|
|
312
|
+
]
|
|
313
|
+
for p in parse_int:
|
|
314
|
+
if p in vote and isinstance(vote[p], int):
|
|
315
|
+
vote[p] = str(vote[p])
|
|
316
|
+
new_active_votes.append(vote)
|
|
317
|
+
output["active_votes"] = new_active_votes
|
|
318
|
+
return json.loads(str(json.dumps(output)))
|
|
319
|
+
|
|
320
|
+
def to_zero(
|
|
321
|
+
self, account: Union[str, Account], partial: float = 100.0, nobroadcast: bool = False
|
|
322
|
+
) -> float:
|
|
323
|
+
"""
|
|
324
|
+
Compute the UI downvote percent needed for `account` to reduce this post's
|
|
325
|
+
pending payout to approximately zero using payout-based math (reward fund
|
|
326
|
+
+ median price + effective vests) scaled by the account's current downvoting power.
|
|
327
|
+
|
|
328
|
+
- Returns a negative UI percent (e.g., -75.0 means a 75% downvote).
|
|
329
|
+
- Automatically casts the downvote (use the blockchain instance's ``no_broadcast``
|
|
330
|
+
flag to suppress broadcasting when needed).
|
|
331
|
+
|
|
332
|
+
If the account's current 100% downvote value is insufficient, returns -100.0.
|
|
333
|
+
If the post has no pending payout, returns 0.0.
|
|
334
|
+
"""
|
|
335
|
+
from .account import Account
|
|
336
|
+
|
|
337
|
+
# Pending payout in HBD
|
|
338
|
+
pending_amt = self.get(
|
|
339
|
+
"pending_payout_value",
|
|
340
|
+
Amount(0, self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain),
|
|
341
|
+
)
|
|
342
|
+
pending_payout = float(Amount(pending_amt, blockchain_instance=self.blockchain))
|
|
343
|
+
if pending_payout <= 0:
|
|
344
|
+
return 0.0
|
|
345
|
+
|
|
346
|
+
# Reward fund and price
|
|
347
|
+
reward_fund = self.blockchain.get_reward_funds()
|
|
348
|
+
recent_claims = int(reward_fund["recent_claims"]) if reward_fund else 0
|
|
349
|
+
if recent_claims == 0:
|
|
350
|
+
return -100.0
|
|
351
|
+
reward_balance = float(
|
|
352
|
+
Amount(reward_fund["reward_balance"], blockchain_instance=self.blockchain)
|
|
353
|
+
)
|
|
354
|
+
median_price = self.blockchain.get_median_price()
|
|
355
|
+
if median_price is None:
|
|
356
|
+
return -100.0
|
|
357
|
+
HBD_per_HIVE = float(
|
|
358
|
+
median_price
|
|
359
|
+
* Amount(1, self.blockchain.token_symbol, blockchain_instance=self.blockchain)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Stake and downvoting power
|
|
363
|
+
acct = Account(account, blockchain_instance=self.blockchain)
|
|
364
|
+
effective_vests = float(acct.get_effective_vesting_shares())
|
|
365
|
+
final_vests = effective_vests * 1e6
|
|
366
|
+
get_dvp = getattr(acct, "get_downvoting_power", None)
|
|
367
|
+
downvote_power_pct = float(get_dvp() if callable(get_dvp) else acct.get_voting_power())
|
|
368
|
+
|
|
369
|
+
# Reference 100% downvote value using provided sample formula
|
|
370
|
+
power = (100 * 100 / 10000.0) / 50.0 # (vote_power * vote_weight / 10000) / 50
|
|
371
|
+
rshares_full = power * final_vests / 10000.0
|
|
372
|
+
current_downvote_value_hbd = (
|
|
373
|
+
(rshares_full * reward_balance / recent_claims) * HBD_per_HIVE
|
|
374
|
+
) / 100.0
|
|
375
|
+
current_downvote_value_hbd *= downvote_power_pct / 100.0
|
|
376
|
+
if current_downvote_value_hbd <= 0:
|
|
377
|
+
return -100.0
|
|
378
|
+
|
|
379
|
+
percent_needed_ui = pending_payout / current_downvote_value_hbd # fraction of a 100% vote
|
|
380
|
+
ui_pct = -percent_needed_ui * (partial / 100.0)
|
|
381
|
+
# Scale to UI percent
|
|
382
|
+
ui_pct_scaled = ui_pct * 100.0
|
|
383
|
+
if ui_pct_scaled < -100.0:
|
|
384
|
+
ui_pct_scaled = -100.0
|
|
385
|
+
if ui_pct_scaled > 0.0:
|
|
386
|
+
ui_pct_scaled = 0.0
|
|
387
|
+
|
|
388
|
+
if ui_pct_scaled < 0.0 and not nobroadcast:
|
|
389
|
+
self.downvote(abs(ui_pct_scaled), voter=account)
|
|
390
|
+
return ui_pct_scaled
|
|
391
|
+
|
|
392
|
+
def to_token_value(
|
|
393
|
+
self,
|
|
394
|
+
account: Union[str, Account],
|
|
395
|
+
hbd: bool = False,
|
|
396
|
+
partial: float = 100.0,
|
|
397
|
+
nobroadcast: bool = False,
|
|
398
|
+
) -> float:
|
|
399
|
+
"""
|
|
400
|
+
Compute the UI upvote percent needed for `account` so the vote contributes
|
|
401
|
+
approximately `hbd` HBD to payout (payout-based math).
|
|
402
|
+
|
|
403
|
+
- Returns a positive UI percent (e.g., 75.0 means 75% upvote).
|
|
404
|
+
- Automatically casts the upvote (use the blockchain instance's ``no_broadcast``
|
|
405
|
+
flag to suppress broadcasting when needed).
|
|
406
|
+
"""
|
|
407
|
+
from .account import Account
|
|
408
|
+
|
|
409
|
+
# Normalize target HBD
|
|
410
|
+
try:
|
|
411
|
+
target_hbd = float(Amount(hbd, blockchain_instance=self.blockchain))
|
|
412
|
+
except Exception:
|
|
413
|
+
target_hbd = float(hbd)
|
|
414
|
+
if target_hbd <= 0:
|
|
415
|
+
return 0.0
|
|
416
|
+
|
|
417
|
+
# Reward fund and price
|
|
418
|
+
reward_fund = self.blockchain.get_reward_funds()
|
|
419
|
+
recent_claims = int(reward_fund["recent_claims"]) if reward_fund else 0
|
|
420
|
+
if recent_claims == 0:
|
|
421
|
+
return 0.0
|
|
422
|
+
reward_balance = float(
|
|
423
|
+
Amount(reward_fund["reward_balance"], blockchain_instance=self.blockchain)
|
|
424
|
+
)
|
|
425
|
+
median_price = self.blockchain.get_median_price()
|
|
426
|
+
if median_price is None:
|
|
427
|
+
return 0.0
|
|
428
|
+
HBD_per_HIVE = float(
|
|
429
|
+
median_price
|
|
430
|
+
* Amount(1, self.blockchain.token_symbol, blockchain_instance=self.blockchain)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Stake and voting power
|
|
434
|
+
acct = Account(account, blockchain_instance=self.blockchain)
|
|
435
|
+
effective_vests = float(acct.get_effective_vesting_shares())
|
|
436
|
+
final_vests = effective_vests * 1e6
|
|
437
|
+
voting_power_pct = float(acct.get_voting_power())
|
|
438
|
+
|
|
439
|
+
# Reference 100% upvote value using provided sample formula
|
|
440
|
+
power = (100 * 100 / 10000.0) / 50.0
|
|
441
|
+
rshares_full = power * final_vests / 10000.0
|
|
442
|
+
current_upvote_value_hbd = (
|
|
443
|
+
(rshares_full * reward_balance / recent_claims) * HBD_per_HIVE
|
|
444
|
+
) / 100.0
|
|
445
|
+
current_upvote_value_hbd *= voting_power_pct / 100.0
|
|
446
|
+
if current_upvote_value_hbd <= 0:
|
|
447
|
+
return 0.0
|
|
448
|
+
|
|
449
|
+
percent_needed_ui = target_hbd / current_upvote_value_hbd # fraction of a 100% vote
|
|
450
|
+
ui_pct = percent_needed_ui * (partial / 100.0)
|
|
451
|
+
# Scale to UI percent
|
|
452
|
+
ui_pct_scaled = ui_pct * 100.0
|
|
453
|
+
if ui_pct_scaled > 100.0:
|
|
454
|
+
ui_pct_scaled = 100.0
|
|
455
|
+
if ui_pct_scaled < 0.0:
|
|
456
|
+
ui_pct_scaled = 0.0
|
|
457
|
+
|
|
458
|
+
if ui_pct_scaled > 0.0 and not nobroadcast:
|
|
459
|
+
self.upvote(ui_pct_scaled, voter=account)
|
|
460
|
+
return ui_pct_scaled
|
|
461
|
+
|
|
462
|
+
@property
|
|
463
|
+
def id(self) -> int:
|
|
464
|
+
return self["id"]
|
|
465
|
+
|
|
466
|
+
@property
|
|
467
|
+
def author(self) -> str:
|
|
468
|
+
return self["author"]
|
|
469
|
+
|
|
470
|
+
@property
|
|
471
|
+
def permlink(self) -> str:
|
|
472
|
+
return self["permlink"]
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def authorperm(self) -> str:
|
|
476
|
+
return construct_authorperm(self["author"], self["permlink"])
|
|
477
|
+
|
|
478
|
+
@property
|
|
479
|
+
def category(self) -> str:
|
|
480
|
+
if "category" in self:
|
|
481
|
+
return self["category"]
|
|
482
|
+
else:
|
|
483
|
+
return ""
|
|
484
|
+
|
|
485
|
+
@property
|
|
486
|
+
def community(self):
|
|
487
|
+
if "community" in self:
|
|
488
|
+
return self["community"]
|
|
489
|
+
else:
|
|
490
|
+
return ""
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def community_title(self):
|
|
494
|
+
"""The Community title property."""
|
|
495
|
+
if "community_title" in self:
|
|
496
|
+
return self["community_title"]
|
|
497
|
+
else:
|
|
498
|
+
return ""
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def parent_author(self):
|
|
502
|
+
if "parent_author" in self:
|
|
503
|
+
return self["parent_author"]
|
|
504
|
+
else:
|
|
505
|
+
return ""
|
|
506
|
+
|
|
507
|
+
@property
|
|
508
|
+
def parent_permlink(self):
|
|
509
|
+
if "parent_permlink" in self:
|
|
510
|
+
return self["parent_permlink"]
|
|
511
|
+
else:
|
|
512
|
+
return ""
|
|
513
|
+
|
|
514
|
+
@property
|
|
515
|
+
def depth(self):
|
|
516
|
+
return self["depth"]
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def title(self):
|
|
520
|
+
if "title" in self:
|
|
521
|
+
return self["title"]
|
|
522
|
+
else:
|
|
523
|
+
return ""
|
|
524
|
+
|
|
525
|
+
@property
|
|
526
|
+
def body(self):
|
|
527
|
+
if "body" in self:
|
|
528
|
+
return self["body"]
|
|
529
|
+
else:
|
|
530
|
+
return ""
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def json_metadata(self):
|
|
534
|
+
if "json_metadata" in self:
|
|
535
|
+
return self["json_metadata"]
|
|
536
|
+
else:
|
|
537
|
+
return {}
|
|
538
|
+
|
|
539
|
+
def is_main_post(self):
|
|
540
|
+
"""Returns True if main post, and False if this is a comment (reply)."""
|
|
541
|
+
if "depth" in self:
|
|
542
|
+
return self["depth"] == 0
|
|
543
|
+
else:
|
|
544
|
+
return self["parent_author"] == ""
|
|
545
|
+
|
|
546
|
+
def is_comment(self):
|
|
547
|
+
"""Returns True if post is a comment"""
|
|
548
|
+
if "depth" in self:
|
|
549
|
+
return self["depth"] > 0
|
|
550
|
+
else:
|
|
551
|
+
return self["parent_author"] != ""
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def reward(self):
|
|
555
|
+
"""
|
|
556
|
+
Return the post's total estimated reward as an Amount.
|
|
557
|
+
|
|
558
|
+
This is the sum of `total_payout_value`, `curator_payout_value`, and `pending_payout_value`
|
|
559
|
+
(from the comment data). Each component is converted to an Amount using the comment's
|
|
560
|
+
blockchain-backed token symbol before summing.
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
Amount: Total estimated reward (in the blockchain's backed token, e.g., HBD).
|
|
564
|
+
"""
|
|
565
|
+
a_zero = Amount(0, self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain)
|
|
566
|
+
author = Amount(self.get("total_payout_value", a_zero), blockchain_instance=self.blockchain)
|
|
567
|
+
curator = Amount(
|
|
568
|
+
self.get("curator_payout_value", a_zero), blockchain_instance=self.blockchain
|
|
569
|
+
)
|
|
570
|
+
pending = Amount(
|
|
571
|
+
self.get("pending_payout_value", a_zero), blockchain_instance=self.blockchain
|
|
572
|
+
)
|
|
573
|
+
return author + curator + pending
|
|
574
|
+
|
|
575
|
+
def is_pending(self):
|
|
576
|
+
"""Returns if the payout is pending (the post/comment
|
|
577
|
+
is younger than 7 days)
|
|
578
|
+
"""
|
|
579
|
+
a_zero = Amount(0, self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain)
|
|
580
|
+
total = Amount(self.get("total_payout_value", a_zero), blockchain_instance=self.blockchain)
|
|
581
|
+
post_age_days = self.time_elapsed().total_seconds() / 60 / 60 / 24
|
|
582
|
+
return post_age_days < 7.0 and float(total) == 0
|
|
583
|
+
|
|
584
|
+
def time_elapsed(self):
|
|
585
|
+
"""
|
|
586
|
+
Return the time elapsed since the post was created as a timedelta.
|
|
587
|
+
|
|
588
|
+
The difference is computed as now (UTC) minus the post's `created` timestamp (a timezone-aware datetime).
|
|
589
|
+
A positive timedelta indicates the post is in the past; a negative value can occur if `created` is in the future.
|
|
590
|
+
"""
|
|
591
|
+
created = self["created"]
|
|
592
|
+
if isinstance(created, str):
|
|
593
|
+
created = parse_time(created)
|
|
594
|
+
return datetime.now(timezone.utc) - created
|
|
595
|
+
|
|
596
|
+
def is_archived(self):
|
|
597
|
+
"""
|
|
598
|
+
Determine whether the post is archived (cashout window passed).
|
|
599
|
+
"""
|
|
600
|
+
cashout = self.get("cashout_time")
|
|
601
|
+
if isinstance(cashout, str):
|
|
602
|
+
cashout = parse_time(cashout)
|
|
603
|
+
if not isinstance(cashout, datetime):
|
|
604
|
+
return False
|
|
605
|
+
return cashout <= datetime(1970, 1, 1, tzinfo=timezone.utc) or cashout < datetime.now(
|
|
606
|
+
timezone.utc
|
|
607
|
+
) - timedelta(days=7)
|
|
608
|
+
|
|
609
|
+
def curation_penalty_compensation_hbd(self):
|
|
610
|
+
"""
|
|
611
|
+
Calculate the HBD payout a post would need (after 15 minutes) to fully compensate the curation penalty for voting earlier than 15 minutes.
|
|
612
|
+
|
|
613
|
+
This refreshes the comment data, selects the reverse-auction window based on the blockchain hardfork (HF6/HF20/HF21), and computes the required payout using the post's current reward and age.
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
Amount: Estimated HBD payout required to offset the early-vote curation penalty.
|
|
617
|
+
"""
|
|
618
|
+
self.refresh()
|
|
619
|
+
# Parse hardfork version (handle strings like "1.28.0")
|
|
620
|
+
if isinstance(self.blockchain.hardfork, str):
|
|
621
|
+
hardfork_version = 25 # Use a reasonable high value for modern Hive
|
|
622
|
+
else:
|
|
623
|
+
hardfork_version = self.blockchain.hardfork
|
|
624
|
+
if hardfork_version >= 21:
|
|
625
|
+
reverse_auction_window_seconds = HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF21
|
|
626
|
+
elif hardfork_version >= 20:
|
|
627
|
+
reverse_auction_window_seconds = HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF20
|
|
628
|
+
else:
|
|
629
|
+
reverse_auction_window_seconds = HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF6
|
|
630
|
+
elapsed_minutes = max((self.time_elapsed()).total_seconds() / 60, 1e-6)
|
|
631
|
+
return self.reward * reverse_auction_window_seconds / (elapsed_minutes**2)
|
|
632
|
+
|
|
633
|
+
def estimate_curation_hbd(self, vote_value_hbd, estimated_value_hbd=None):
|
|
634
|
+
"""
|
|
635
|
+
Estimate the curation reward (in HBD) for a given vote on this post.
|
|
636
|
+
|
|
637
|
+
Refreshes the post data from the chain before computing. If `estimated_value_hbd` is not provided, the current post reward is used as the estimated total post value. The returned value is an estimate of the curator's HBD payout for a vote of size `vote_value_hbd`, accounting for the current curation penalty.
|
|
638
|
+
|
|
639
|
+
Parameters:
|
|
640
|
+
vote_value_hbd (float): Vote value in HBD used to compute the curation share.
|
|
641
|
+
estimated_value_hbd (float, optional): Estimated total post value in HBD to scale the curation; defaults to the post's current reward.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
float: Estimated curation reward in HBD for the provided vote value.
|
|
645
|
+
"""
|
|
646
|
+
self.refresh()
|
|
647
|
+
if estimated_value_hbd is None:
|
|
648
|
+
estimated_value_hbd = float(self.reward)
|
|
649
|
+
t = 1.0 - self.get_curation_penalty()
|
|
650
|
+
k = vote_value_hbd / (vote_value_hbd + float(self.reward))
|
|
651
|
+
K = (1 - math.sqrt(1 - k)) / 4 / k
|
|
652
|
+
return K * vote_value_hbd * t * math.sqrt(estimated_value_hbd)
|
|
653
|
+
|
|
654
|
+
def get_curation_penalty(self, vote_time=None):
|
|
655
|
+
"""
|
|
656
|
+
Return the curation penalty factor for a vote at a given time.
|
|
657
|
+
|
|
658
|
+
Calculates a value in [0.0, 1.0] representing the fraction of curation rewards
|
|
659
|
+
that will be removed due to early voting (0.0 = no penalty, 1.0 = full penalty).
|
|
660
|
+
The penalty is based on the elapsed time between the post's creation and
|
|
661
|
+
the vote time, scaled by the Hive reverse-auction window for the node's
|
|
662
|
+
current hardfork (HF21, HF20, or HF6).
|
|
663
|
+
|
|
664
|
+
Parameters:
|
|
665
|
+
vote_time (datetime | date | str | None): Time of the vote. If None,
|
|
666
|
+
the current time is used. If a string is given it will be parsed
|
|
667
|
+
with the module's time formatter.
|
|
668
|
+
|
|
669
|
+
Returns:
|
|
670
|
+
float: Penalty fraction in the range [0.0, 1.0].
|
|
671
|
+
|
|
672
|
+
Raises:
|
|
673
|
+
ValueError: If vote_time is not None and not a datetime, date, or parseable string.
|
|
674
|
+
"""
|
|
675
|
+
if vote_time is None:
|
|
676
|
+
elapsed_seconds = self.time_elapsed().total_seconds()
|
|
677
|
+
elif isinstance(vote_time, str):
|
|
678
|
+
elapsed_seconds = (formatTimeString(vote_time) - self["created"]).total_seconds()
|
|
679
|
+
elif isinstance(vote_time, (datetime, date)):
|
|
680
|
+
elapsed_seconds = (vote_time - self["created"]).total_seconds()
|
|
681
|
+
else:
|
|
682
|
+
raise ValueError("vote_time must be a string or a datetime")
|
|
683
|
+
# Parse hardfork version (handle strings like "1.28.0")
|
|
684
|
+
if isinstance(self.blockchain.hardfork, str):
|
|
685
|
+
hardfork_version = 25 # Use a reasonable high value for modern Hive
|
|
686
|
+
else:
|
|
687
|
+
hardfork_version = self.blockchain.hardfork
|
|
688
|
+
if hardfork_version >= 21:
|
|
689
|
+
reward = elapsed_seconds / HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF21
|
|
690
|
+
elif hardfork_version >= 20:
|
|
691
|
+
reward = elapsed_seconds / HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF20
|
|
692
|
+
else:
|
|
693
|
+
reward = elapsed_seconds / HIVE_REVERSE_AUCTION_WINDOW_SECONDS_HF6
|
|
694
|
+
if reward > 1:
|
|
695
|
+
reward = 1.0
|
|
696
|
+
return 1.0 - reward
|
|
697
|
+
|
|
698
|
+
def get_vote_with_curation(self, voter=None, raw_data=False, pending_payout_value=None):
|
|
699
|
+
"""
|
|
700
|
+
Return the specified voter's vote for this comment, optionally augmented with curation data.
|
|
701
|
+
|
|
702
|
+
If `voter` is not found in the comment's votes returns None. When a vote is found:
|
|
703
|
+
- If `raw_data` is True or the post is not pending payout, returns the raw vote dict.
|
|
704
|
+
- If the post is pending and `raw_data` is False, returns the vote dict augmented with:
|
|
705
|
+
- `curation_reward`: the vote's curation reward (in HBD)
|
|
706
|
+
- `ROI`: percent return on the voter's effective voting value
|
|
707
|
+
|
|
708
|
+
Parameters:
|
|
709
|
+
voter (str or Account, optional): Voter name or Account. If omitted, defaults to the post author as an Account.
|
|
710
|
+
raw_data (bool, optional): If True, return the found vote without adding curation/ROI fields.
|
|
711
|
+
pending_payout_value (float or str, optional): If provided, use this HBD value instead of the current pending payout when computing curation rewards.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
dict or None: The vote dictionary (possibly augmented with `curation_reward` and `ROI`) or None if the voter has not voted.
|
|
715
|
+
"""
|
|
716
|
+
specific_vote = None
|
|
717
|
+
if voter is None:
|
|
718
|
+
voter = Account(self["author"], blockchain_instance=self.blockchain)
|
|
719
|
+
else:
|
|
720
|
+
voter = Account(voter, blockchain_instance=self.blockchain)
|
|
721
|
+
if "active_votes" in self:
|
|
722
|
+
for vote in self["active_votes"]:
|
|
723
|
+
if voter["name"] == vote["voter"]:
|
|
724
|
+
specific_vote = vote
|
|
725
|
+
else:
|
|
726
|
+
active_votes = self.get_votes()
|
|
727
|
+
for vote in active_votes:
|
|
728
|
+
if voter["name"] == vote["voter"]:
|
|
729
|
+
specific_vote = vote
|
|
730
|
+
if specific_vote is not None and (raw_data or not self.is_pending()):
|
|
731
|
+
return specific_vote
|
|
732
|
+
elif specific_vote is not None:
|
|
733
|
+
curation_reward = self.get_curation_rewards(
|
|
734
|
+
pending_payout_hbd=True, pending_payout_value=pending_payout_value
|
|
735
|
+
)
|
|
736
|
+
specific_vote["curation_reward"] = curation_reward["active_votes"][voter["name"]]
|
|
737
|
+
specific_vote["ROI"] = (
|
|
738
|
+
float(curation_reward["active_votes"][voter["name"]])
|
|
739
|
+
/ float(
|
|
740
|
+
voter.get_voting_value(
|
|
741
|
+
voting_power=None, voting_weight=specific_vote["percent"] / 100
|
|
742
|
+
)
|
|
743
|
+
)
|
|
744
|
+
* 100
|
|
745
|
+
)
|
|
746
|
+
return specific_vote
|
|
747
|
+
else:
|
|
748
|
+
return None
|
|
749
|
+
|
|
750
|
+
def get_beneficiaries_pct(self):
|
|
751
|
+
"""
|
|
752
|
+
Return the sum of beneficiary weights as a fraction of the full payout.
|
|
753
|
+
|
|
754
|
+
If the post has a `beneficiaries` list of dicts with integer `weight` fields (0–10000 representing 0%–100%), this returns the total weight divided by 100.0 (i.e., a float in 0.0–100.0/100 range; typical values are 0.0–1.0).
|
|
755
|
+
"""
|
|
756
|
+
beneficiaries = self["beneficiaries"]
|
|
757
|
+
weight = 0
|
|
758
|
+
for b in beneficiaries:
|
|
759
|
+
weight += b["weight"]
|
|
760
|
+
return weight / HIVE_100_PERCENT
|
|
761
|
+
|
|
762
|
+
def get_rewards(self):
|
|
763
|
+
"""
|
|
764
|
+
Return the post's total, author, and curator payouts as Amount objects (HBD).
|
|
765
|
+
|
|
766
|
+
If the post is pending, returns an estimated total based on pending_payout_value and derives the author's share via get_author_rewards(); curator_payout is computed as the difference. For finalized posts, uses total_payout_value and curator_payout_value.
|
|
767
|
+
|
|
768
|
+
Note: beneficiary rewards (if any) are already deducted from the returned author_payout and total_payout.
|
|
769
|
+
|
|
770
|
+
Returns:
|
|
771
|
+
dict: {
|
|
772
|
+
"total_payout": Amount,
|
|
773
|
+
"author_payout": Amount,
|
|
774
|
+
"curator_payout": Amount,
|
|
775
|
+
}
|
|
776
|
+
"""
|
|
777
|
+
if self.is_pending():
|
|
778
|
+
total_payout = Amount(self["pending_payout_value"], blockchain_instance=self.blockchain)
|
|
779
|
+
author_payout = self.get_author_rewards()["total_payout_HBD"]
|
|
780
|
+
curator_payout = total_payout - author_payout
|
|
781
|
+
else:
|
|
782
|
+
author_payout = Amount(self["total_payout_value"], blockchain_instance=self.blockchain)
|
|
783
|
+
curator_payout = Amount(
|
|
784
|
+
self["curator_payout_value"], blockchain_instance=self.blockchain
|
|
785
|
+
)
|
|
786
|
+
total_payout = author_payout + curator_payout
|
|
787
|
+
return {
|
|
788
|
+
"total_payout": total_payout,
|
|
789
|
+
"author_payout": author_payout,
|
|
790
|
+
"curator_payout": curator_payout,
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
def get_author_rewards(self):
|
|
794
|
+
"""
|
|
795
|
+
Return the computed author-side rewards for this post.
|
|
796
|
+
|
|
797
|
+
If the post payout is not pending, returns zero HP/HBD payouts and the concrete total payout as `total_payout_HBD`. If the payout is pending, computes the author’s share after curation and beneficiaries, and—when price history and percent_hbd are available—splits that share into HBD and HP equivalents.
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
dict: A dictionary with the following keys:
|
|
801
|
+
- pending_rewards (bool): True when the post payout is still pending.
|
|
802
|
+
- payout_HP (Amount or None): Estimated Hive Power payout (Amount) when pending and convertible; otherwise 0 Amount (when not pending) or None.
|
|
803
|
+
- payout_HBD (Amount or None): Estimated HBD payout (Amount) when pending and convertible; otherwise 0 Amount (when not pending) or None.
|
|
804
|
+
- total_payout_HBD (Amount): Total author-side payout expressed in HBD-equivalent units when pending, or the concrete total payout when not pending.
|
|
805
|
+
- total_payout (Amount, optional): Present only for pending payouts in the non-convertible branch; the author-side token amount before HBD/HP splitting.
|
|
806
|
+
- Note: When price/percent data is not available, `payout_HP` and `payout_HBD` will be None and only `total_payout_HBD`/`total_payout` convey the author share.
|
|
807
|
+
|
|
808
|
+
Example:
|
|
809
|
+
{
|
|
810
|
+
"pending_rewards": True,
|
|
811
|
+
"payout_HP": Amount(...), # HP equivalent (when convertible)
|
|
812
|
+
"payout_HBD": Amount(...), # HBD portion (when convertible)
|
|
813
|
+
"total_payout_HBD": Amount(...) # Total author share in HBD-equivalent
|
|
814
|
+
}
|
|
815
|
+
"""
|
|
816
|
+
if not self.is_pending():
|
|
817
|
+
return {
|
|
818
|
+
"pending_rewards": False,
|
|
819
|
+
"payout_HP": Amount(
|
|
820
|
+
0, self.blockchain.token_symbol, blockchain_instance=self.blockchain
|
|
821
|
+
),
|
|
822
|
+
"payout_HBD": Amount(
|
|
823
|
+
0, self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain
|
|
824
|
+
),
|
|
825
|
+
"total_payout_HBD": Amount(
|
|
826
|
+
self["total_payout_value"], blockchain_instance=self.blockchain
|
|
827
|
+
),
|
|
828
|
+
}
|
|
829
|
+
author_reward_factor = 0.5
|
|
830
|
+
median_hist = self.blockchain.get_current_median_history()
|
|
831
|
+
if median_hist is not None:
|
|
832
|
+
median_price = Price(median_hist, blockchain_instance=self.blockchain)
|
|
833
|
+
beneficiaries_pct = self.get_beneficiaries_pct()
|
|
834
|
+
curation_tokens = self.reward * author_reward_factor
|
|
835
|
+
author_tokens = self.reward - curation_tokens
|
|
836
|
+
curation_rewards = self.get_curation_rewards()
|
|
837
|
+
|
|
838
|
+
# Parse hardfork version (handle strings like "1.28.0")
|
|
839
|
+
if isinstance(self.blockchain.hardfork, str):
|
|
840
|
+
# For version strings like "1.28.0", we only care about the major version (1)
|
|
841
|
+
# Since all Hive hardforks we're checking against (20, 21) are much higher
|
|
842
|
+
# than 1, we can safely treat any version string as being >= these versions
|
|
843
|
+
hardfork_version = 25 # Use a reasonable high value for modern Hive
|
|
844
|
+
else:
|
|
845
|
+
hardfork_version = self.blockchain.hardfork
|
|
846
|
+
|
|
847
|
+
if hardfork_version >= 20 and median_hist is not None:
|
|
848
|
+
author_tokens += median_price * curation_rewards["unclaimed_rewards"]
|
|
849
|
+
benefactor_tokens = author_tokens * beneficiaries_pct / HIVE_100_PERCENT
|
|
850
|
+
author_tokens -= benefactor_tokens
|
|
851
|
+
|
|
852
|
+
if median_hist is not None and "percent_hbd" in self:
|
|
853
|
+
hbd_payout = author_tokens * self["percent_hbd"] / 20000.0
|
|
854
|
+
hp_payout = median_price.as_base(self.blockchain.token_symbol) * (
|
|
855
|
+
author_tokens - hbd_payout
|
|
856
|
+
)
|
|
857
|
+
return {
|
|
858
|
+
"pending_rewards": True,
|
|
859
|
+
"payout_HP": hp_payout,
|
|
860
|
+
"payout_HBD": hbd_payout,
|
|
861
|
+
"total_payout_HBD": author_tokens,
|
|
862
|
+
}
|
|
863
|
+
else:
|
|
864
|
+
return {
|
|
865
|
+
"pending_rewards": True,
|
|
866
|
+
"total_payout": author_tokens,
|
|
867
|
+
# HBD/HP primary fields
|
|
868
|
+
"total_payout_HBD": author_tokens,
|
|
869
|
+
"payout_HBD": None,
|
|
870
|
+
"payout_HP": None,
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
def get_curation_rewards(self, pending_payout_hbd=False, pending_payout_value=None):
|
|
874
|
+
"""
|
|
875
|
+
Calculate curation rewards for this post and distribute them across active voters.
|
|
876
|
+
|
|
877
|
+
Parameters:
|
|
878
|
+
pending_payout_hbd (bool): If True, compute and return rewards in HBD (do not convert to HIVE/HP). Default False.
|
|
879
|
+
pending_payout_value (float | str | Amount | None): Optional override for the post's pending payout value used when the post is still pending.
|
|
880
|
+
- If None and the post is pending, the function uses the post's stored pending_payout_value.
|
|
881
|
+
- Accepted types: numeric, string amount, or an Amount instance.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
dict: {
|
|
885
|
+
"pending_rewards": bool, # True if the post is still within the payout window (uses pending_payout_value)
|
|
886
|
+
"unclaimed_rewards": Amount, # Amount reserved for unclaimed curation (e.g., self-votes or early votes)
|
|
887
|
+
"active_votes": dict # Mapping voter_name -> Amount of curation reward allocated to that voter
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
Notes:
|
|
891
|
+
- The function splits the curation pool using the protocol's curator share (50% by default) and prorates per-voter claims by vote weight.
|
|
892
|
+
- When a current median price history is available, rewards may be converted between HBD and the chain's token (HP) according to pending_payout_hbd.
|
|
893
|
+
"""
|
|
894
|
+
median_hist = self.blockchain.get_current_median_history()
|
|
895
|
+
if median_hist is not None:
|
|
896
|
+
median_price = Price(median_hist, blockchain_instance=self.blockchain)
|
|
897
|
+
pending_rewards = False
|
|
898
|
+
active_votes_list = self.get_votes()
|
|
899
|
+
curator_reward_factor = 0.5
|
|
900
|
+
|
|
901
|
+
if "total_vote_weight" in self:
|
|
902
|
+
total_vote_weight = self["total_vote_weight"]
|
|
903
|
+
active_votes_json_list = []
|
|
904
|
+
for vote in active_votes_list:
|
|
905
|
+
if "weight" not in vote:
|
|
906
|
+
vote.refresh()
|
|
907
|
+
active_votes_json_list.append(vote.json())
|
|
908
|
+
else:
|
|
909
|
+
active_votes_json_list.append(vote.json())
|
|
910
|
+
|
|
911
|
+
total_vote_weight = 0
|
|
912
|
+
for vote in active_votes_json_list:
|
|
913
|
+
total_vote_weight += vote["weight"]
|
|
914
|
+
|
|
915
|
+
if not self.is_pending():
|
|
916
|
+
if pending_payout_hbd or median_hist is None:
|
|
917
|
+
max_rewards = Amount(
|
|
918
|
+
self["curator_payout_value"], blockchain_instance=self.blockchain
|
|
919
|
+
)
|
|
920
|
+
else:
|
|
921
|
+
max_rewards = median_price.as_base(self.blockchain.token_symbol) * Amount(
|
|
922
|
+
self["curator_payout_value"], blockchain_instance=self.blockchain
|
|
923
|
+
)
|
|
924
|
+
unclaimed_rewards = Amount(
|
|
925
|
+
0, self.blockchain.token_symbol, blockchain_instance=self.blockchain
|
|
926
|
+
)
|
|
927
|
+
else:
|
|
928
|
+
if pending_payout_value is None and "pending_payout_value" in self:
|
|
929
|
+
pending_payout_value = Amount(
|
|
930
|
+
self["pending_payout_value"], blockchain_instance=self.blockchain
|
|
931
|
+
)
|
|
932
|
+
elif pending_payout_value is None:
|
|
933
|
+
pending_payout_value = 0
|
|
934
|
+
elif isinstance(pending_payout_value, (float, int)):
|
|
935
|
+
pending_payout_value = Amount(
|
|
936
|
+
pending_payout_value,
|
|
937
|
+
self.blockchain.backed_token_symbol,
|
|
938
|
+
blockchain_instance=self.blockchain,
|
|
939
|
+
)
|
|
940
|
+
elif isinstance(pending_payout_value, str):
|
|
941
|
+
pending_payout_value = Amount(
|
|
942
|
+
pending_payout_value, blockchain_instance=self.blockchain
|
|
943
|
+
)
|
|
944
|
+
if pending_payout_hbd or median_hist is None:
|
|
945
|
+
max_rewards = pending_payout_value * curator_reward_factor
|
|
946
|
+
else:
|
|
947
|
+
max_rewards = median_price.as_base(self.blockchain.token_symbol) * (
|
|
948
|
+
pending_payout_value * curator_reward_factor
|
|
949
|
+
)
|
|
950
|
+
# max_rewards is an Amount, ensure we have a copy
|
|
951
|
+
if isinstance(max_rewards, Amount):
|
|
952
|
+
unclaimed_rewards = max_rewards.copy()
|
|
953
|
+
else:
|
|
954
|
+
# Convert to Amount if it's not already
|
|
955
|
+
unclaimed_rewards = Amount(max_rewards, blockchain_instance=self.blockchain)
|
|
956
|
+
pending_rewards = True
|
|
957
|
+
|
|
958
|
+
active_votes = {}
|
|
959
|
+
|
|
960
|
+
for vote in active_votes_json_list:
|
|
961
|
+
if total_vote_weight > 0:
|
|
962
|
+
claim = max_rewards * int(vote["weight"]) / total_vote_weight
|
|
963
|
+
else:
|
|
964
|
+
claim = 0
|
|
965
|
+
if claim > 0 and pending_rewards:
|
|
966
|
+
# Ensure claim is an Amount for subtraction
|
|
967
|
+
if not isinstance(claim, Amount):
|
|
968
|
+
claim_amount = Amount(claim, blockchain_instance=self.blockchain)
|
|
969
|
+
else:
|
|
970
|
+
claim_amount = claim
|
|
971
|
+
unclaimed_rewards = unclaimed_rewards - claim_amount
|
|
972
|
+
if claim > 0:
|
|
973
|
+
active_votes[vote["voter"]] = claim
|
|
974
|
+
else:
|
|
975
|
+
active_votes[vote["voter"]] = 0
|
|
976
|
+
|
|
977
|
+
return {
|
|
978
|
+
"pending_rewards": pending_rewards,
|
|
979
|
+
"unclaimed_rewards": unclaimed_rewards,
|
|
980
|
+
"active_votes": active_votes,
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
def get_reblogged_by(self, identifier=None):
|
|
984
|
+
"""Shows in which blogs this post appears"""
|
|
985
|
+
if not identifier:
|
|
986
|
+
post_author = self["author"]
|
|
987
|
+
post_permlink = self["permlink"]
|
|
988
|
+
else:
|
|
989
|
+
[post_author, post_permlink] = resolve_authorperm(identifier)
|
|
990
|
+
if not self.blockchain.is_connected():
|
|
991
|
+
return None
|
|
992
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
993
|
+
try:
|
|
994
|
+
return self.blockchain.rpc.get_reblogged_by(post_author, post_permlink)["accounts"]
|
|
995
|
+
except Exception:
|
|
996
|
+
return self.blockchain.rpc.get_reblogged_by([post_author, post_permlink])["accounts"]
|
|
997
|
+
|
|
998
|
+
def get_replies(self, raw_data=False, identifier=None):
|
|
999
|
+
"""Returns content replies
|
|
1000
|
+
|
|
1001
|
+
:param bool raw_data: When set to False, the replies will be returned as Comment class objects
|
|
1002
|
+
"""
|
|
1003
|
+
if not identifier:
|
|
1004
|
+
post_author = self["author"]
|
|
1005
|
+
post_permlink = self["permlink"]
|
|
1006
|
+
else:
|
|
1007
|
+
[post_author, post_permlink] = resolve_authorperm(identifier)
|
|
1008
|
+
if not self.blockchain.is_connected():
|
|
1009
|
+
return None
|
|
1010
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1011
|
+
|
|
1012
|
+
# Use bridge.get_discussion API
|
|
1013
|
+
content_replies = self.blockchain.rpc.get_discussion(
|
|
1014
|
+
{"author": post_author, "permlink": post_permlink, "observer": self.observer},
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
if not content_replies:
|
|
1018
|
+
return []
|
|
1019
|
+
|
|
1020
|
+
# The response format is a dict with keys in 'author/permlink' format
|
|
1021
|
+
# We need to extract the replies by filtering out the original post
|
|
1022
|
+
original_key = f"{post_author}/{post_permlink}"
|
|
1023
|
+
replies = []
|
|
1024
|
+
|
|
1025
|
+
for key, content in content_replies.items():
|
|
1026
|
+
# Skip the original post
|
|
1027
|
+
if key == original_key:
|
|
1028
|
+
continue
|
|
1029
|
+
# Add the reply
|
|
1030
|
+
replies.append(content)
|
|
1031
|
+
|
|
1032
|
+
if raw_data:
|
|
1033
|
+
return replies
|
|
1034
|
+
return [Comment(c, blockchain_instance=self.blockchain) for c in replies]
|
|
1035
|
+
|
|
1036
|
+
def get_all_replies(self, parent=None):
|
|
1037
|
+
"""Returns all content replies"""
|
|
1038
|
+
if parent is None:
|
|
1039
|
+
parent = self
|
|
1040
|
+
if parent["children"] > 0:
|
|
1041
|
+
children = parent.get_replies()
|
|
1042
|
+
if children is None:
|
|
1043
|
+
return []
|
|
1044
|
+
for cc in children[:]:
|
|
1045
|
+
children.extend(self.get_all_replies(parent=cc))
|
|
1046
|
+
return children
|
|
1047
|
+
return []
|
|
1048
|
+
|
|
1049
|
+
def get_parent(self, children=None):
|
|
1050
|
+
"""Returns the parent post with depth == 0"""
|
|
1051
|
+
if children is None:
|
|
1052
|
+
children = self
|
|
1053
|
+
while children["depth"] > 0:
|
|
1054
|
+
children = Comment(
|
|
1055
|
+
construct_authorperm(children["parent_author"], children["parent_permlink"]),
|
|
1056
|
+
blockchain_instance=self.blockchain,
|
|
1057
|
+
)
|
|
1058
|
+
return children
|
|
1059
|
+
|
|
1060
|
+
def get_votes(self, raw_data=False):
|
|
1061
|
+
"""Returns all votes as ActiveVotes object"""
|
|
1062
|
+
if raw_data and "active_votes" in self:
|
|
1063
|
+
return self["active_votes"]
|
|
1064
|
+
from .vote import ActiveVotes
|
|
1065
|
+
|
|
1066
|
+
authorperm = construct_authorperm(self["author"], self["permlink"])
|
|
1067
|
+
return ActiveVotes(authorperm, lazy=False, blockchain_instance=self.blockchain)
|
|
1068
|
+
|
|
1069
|
+
def upvote(
|
|
1070
|
+
self, weight: float = 100.0, voter: Optional[Union[str, Account]] = None
|
|
1071
|
+
) -> Dict[str, Any]:
|
|
1072
|
+
"""Upvote the post
|
|
1073
|
+
|
|
1074
|
+
:param float weight: (optional) Weight for posting (-100.0 -
|
|
1075
|
+
+100.0) defaults to +100.0
|
|
1076
|
+
:param str voter: (optional) Voting account
|
|
1077
|
+
|
|
1078
|
+
"""
|
|
1079
|
+
if weight < 0:
|
|
1080
|
+
raise ValueError("Weight must be >= 0.")
|
|
1081
|
+
last_payout = self.get("last_payout", None)
|
|
1082
|
+
if last_payout is not None:
|
|
1083
|
+
if formatToTimeStamp(last_payout) > 0:
|
|
1084
|
+
raise VotingInvalidOnArchivedPost
|
|
1085
|
+
return self.vote(weight, account=voter)
|
|
1086
|
+
|
|
1087
|
+
def downvote(
|
|
1088
|
+
self, weight: float = 100.0, voter: Optional[Union[str, Account]] = None
|
|
1089
|
+
) -> Dict[str, Any]:
|
|
1090
|
+
"""Downvote the post
|
|
1091
|
+
|
|
1092
|
+
:param float weight: (optional) Weight for posting (-100.0 -
|
|
1093
|
+
+100.0) defaults to -100.0
|
|
1094
|
+
:param str voter: (optional) Voting account
|
|
1095
|
+
|
|
1096
|
+
"""
|
|
1097
|
+
if weight < 0:
|
|
1098
|
+
raise ValueError("Weight must be >= 0.")
|
|
1099
|
+
last_payout = self.get("last_payout", None)
|
|
1100
|
+
if last_payout is not None:
|
|
1101
|
+
if formatToTimeStamp(last_payout) > 0:
|
|
1102
|
+
raise VotingInvalidOnArchivedPost
|
|
1103
|
+
return self.vote(-weight, account=voter)
|
|
1104
|
+
|
|
1105
|
+
def vote(
|
|
1106
|
+
self,
|
|
1107
|
+
weight: float,
|
|
1108
|
+
account: Optional[Union[str, Account]] = None,
|
|
1109
|
+
identifier: Optional[str] = None,
|
|
1110
|
+
**kwargs: Any,
|
|
1111
|
+
) -> Dict[str, Any]:
|
|
1112
|
+
"""Vote for a post
|
|
1113
|
+
|
|
1114
|
+
:param float weight: Voting weight. Range: -100.0 - +100.0.
|
|
1115
|
+
:param str account: (optional) Account to use for voting. If
|
|
1116
|
+
``account`` is not defined, the ``default_account`` will be used
|
|
1117
|
+
or a ValueError will be raised
|
|
1118
|
+
:param str identifier: Identifier for the post to vote. Takes the
|
|
1119
|
+
form ``@author/permlink``.
|
|
1120
|
+
|
|
1121
|
+
"""
|
|
1122
|
+
if not identifier:
|
|
1123
|
+
identifier = construct_authorperm(self["author"], self["permlink"])
|
|
1124
|
+
|
|
1125
|
+
return self.blockchain.vote(weight, identifier, account=account)
|
|
1126
|
+
|
|
1127
|
+
def edit(self, body, meta=None, replace=False):
|
|
1128
|
+
"""Edit an existing post
|
|
1129
|
+
|
|
1130
|
+
:param str body: Body of the reply
|
|
1131
|
+
:param json meta: JSON meta object that can be attached to the
|
|
1132
|
+
post. (optional)
|
|
1133
|
+
:param bool replace: Instead of calculating a *diff*, replace
|
|
1134
|
+
the post entirely (defaults to ``False``)
|
|
1135
|
+
|
|
1136
|
+
"""
|
|
1137
|
+
if not meta:
|
|
1138
|
+
meta = {}
|
|
1139
|
+
original_post = self
|
|
1140
|
+
|
|
1141
|
+
if replace:
|
|
1142
|
+
newbody = body
|
|
1143
|
+
else:
|
|
1144
|
+
newbody = make_patch(original_post["body"], body)
|
|
1145
|
+
if not newbody:
|
|
1146
|
+
log.info("No changes made! Skipping ...")
|
|
1147
|
+
return
|
|
1148
|
+
|
|
1149
|
+
reply_identifier = construct_authorperm(
|
|
1150
|
+
original_post["parent_author"], original_post["parent_permlink"]
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
new_meta = {}
|
|
1154
|
+
if meta is not None:
|
|
1155
|
+
if bool(original_post["json_metadata"]):
|
|
1156
|
+
new_meta = original_post["json_metadata"]
|
|
1157
|
+
for key in meta:
|
|
1158
|
+
new_meta[key] = meta[key]
|
|
1159
|
+
else:
|
|
1160
|
+
new_meta = meta
|
|
1161
|
+
|
|
1162
|
+
return self.blockchain.post(
|
|
1163
|
+
original_post["title"],
|
|
1164
|
+
newbody,
|
|
1165
|
+
reply_identifier=reply_identifier,
|
|
1166
|
+
author=original_post["author"],
|
|
1167
|
+
permlink=original_post["permlink"],
|
|
1168
|
+
json_metadata=new_meta,
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
def reply(self, body, title="", author="", meta=None):
|
|
1172
|
+
"""Reply to an existing post
|
|
1173
|
+
|
|
1174
|
+
:param str body: Body of the reply
|
|
1175
|
+
:param str title: Title of the reply post
|
|
1176
|
+
:param str author: Author of reply (optional) if not provided
|
|
1177
|
+
``default_user`` will be used, if present, else
|
|
1178
|
+
a ``ValueError`` will be raised.
|
|
1179
|
+
:param json meta: JSON meta object that can be attached to the
|
|
1180
|
+
post. (optional)
|
|
1181
|
+
|
|
1182
|
+
"""
|
|
1183
|
+
return self.blockchain.post(
|
|
1184
|
+
title, body, json_metadata=meta, author=author, reply_identifier=self.identifier
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
def delete(self, account=None, identifier=None):
|
|
1188
|
+
"""
|
|
1189
|
+
Delete this post or comment from the blockchain.
|
|
1190
|
+
|
|
1191
|
+
If `identifier` is provided it must be an author/permlink string (e.g. "@author/permlink"); otherwise the current Comment's author and permlink are used. If `account` is not provided the method will use `blockchain.config["default_account"]` when present; otherwise a ValueError is raised.
|
|
1192
|
+
|
|
1193
|
+
Note: a post/comment can only be deleted if it has no replies and no positive rshares.
|
|
1194
|
+
|
|
1195
|
+
Parameters:
|
|
1196
|
+
account (str, optional): Account name to perform the deletion. If omitted, the configured default_account is used.
|
|
1197
|
+
identifier (str, optional): Author/permlink of the post to delete (format "@author/permlink"). Defaults to the current Comment.
|
|
1198
|
+
|
|
1199
|
+
Returns:
|
|
1200
|
+
dict: Result of the blockchain finalizeOp / transaction broadcast.
|
|
1201
|
+
|
|
1202
|
+
Raises:
|
|
1203
|
+
ValueError: If no account is provided and no default_account is configured.
|
|
1204
|
+
"""
|
|
1205
|
+
if not account:
|
|
1206
|
+
if "default_account" in self.blockchain.config:
|
|
1207
|
+
account = self.blockchain.config["default_account"]
|
|
1208
|
+
if not account:
|
|
1209
|
+
raise ValueError("You need to provide an account")
|
|
1210
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
1211
|
+
if not identifier:
|
|
1212
|
+
post_author = self["author"]
|
|
1213
|
+
post_permlink = self["permlink"]
|
|
1214
|
+
else:
|
|
1215
|
+
[post_author, post_permlink] = resolve_authorperm(identifier)
|
|
1216
|
+
op = operations.Delete_comment(**{"author": post_author, "permlink": post_permlink})
|
|
1217
|
+
return self.blockchain.finalizeOp(op, account, "posting")
|
|
1218
|
+
|
|
1219
|
+
def reblog(self, identifier=None, account=None):
|
|
1220
|
+
"""
|
|
1221
|
+
Create a reblog (resteem) for the specified post.
|
|
1222
|
+
|
|
1223
|
+
Parameters:
|
|
1224
|
+
identifier (str, optional): Post identifier in the form "@author/permlink". If omitted, uses this Comment's identifier.
|
|
1225
|
+
account (str, optional): Name of the posting account to perform the reblog. If omitted, the configured `default_account` is used.
|
|
1226
|
+
|
|
1227
|
+
Returns:
|
|
1228
|
+
dict: Result from the blockchain custom_json operation.
|
|
1229
|
+
|
|
1230
|
+
Raises:
|
|
1231
|
+
ValueError: If no account is provided and no `default_account` is configured.
|
|
1232
|
+
"""
|
|
1233
|
+
if not account:
|
|
1234
|
+
account = self.blockchain.configStorage.get("default_account")
|
|
1235
|
+
if not account:
|
|
1236
|
+
raise ValueError("You need to provide an account")
|
|
1237
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
1238
|
+
if identifier is None:
|
|
1239
|
+
identifier = self.identifier
|
|
1240
|
+
if identifier is None:
|
|
1241
|
+
raise ValueError("No identifier available")
|
|
1242
|
+
author, permlink = resolve_authorperm(str(identifier))
|
|
1243
|
+
json_body = ["reblog", {"account": account["name"], "author": author, "permlink": permlink}]
|
|
1244
|
+
return self.blockchain.custom_json(
|
|
1245
|
+
id="follow", json_data=json_body, required_posting_auths=[account["name"]]
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
class RecentReplies(list):
|
|
1250
|
+
"""Obtain a list of recent replies
|
|
1251
|
+
|
|
1252
|
+
:param str author: author
|
|
1253
|
+
:param bool skip_own: (optional) Skip replies of the author to him/herself.
|
|
1254
|
+
Default: True
|
|
1255
|
+
:param Blockchain blockchain_instance: Blockchain instance to use when accessing the RPC
|
|
1256
|
+
"""
|
|
1257
|
+
|
|
1258
|
+
def __init__(
|
|
1259
|
+
self,
|
|
1260
|
+
author,
|
|
1261
|
+
skip_own=True,
|
|
1262
|
+
start_permlink="",
|
|
1263
|
+
limit=100,
|
|
1264
|
+
lazy=False,
|
|
1265
|
+
full=True,
|
|
1266
|
+
blockchain_instance=None,
|
|
1267
|
+
):
|
|
1268
|
+
"""
|
|
1269
|
+
Create a list of recent replies to a given account.
|
|
1270
|
+
|
|
1271
|
+
Initializes the instance as a list of Comment objects built from the account's recent "replies" feed. By default replies authored by the same account are omitted when skip_own is True. If no blockchain connection is available during construction, initialization is aborted (the constructor returns early).
|
|
1272
|
+
|
|
1273
|
+
Parameters:
|
|
1274
|
+
author (str): Account name whose replies to collect.
|
|
1275
|
+
skip_own (bool): If True, omit replies authored by `author`. Default True.
|
|
1276
|
+
start_permlink (str): Legacy/paging parameter; currently ignored by this implementation.
|
|
1277
|
+
limit (int): Maximum number of replies to collect; currently ignored (the underlying API call controls results).
|
|
1278
|
+
lazy (bool): If True, create Comment objects in lazy mode.
|
|
1279
|
+
full (bool): If True, create Comment objects with full data populated.
|
|
1280
|
+
|
|
1281
|
+
Notes:
|
|
1282
|
+
- The blockchain_instance parameter is used to resolve RPC access and is intentionally undocumented here as a shared service.
|
|
1283
|
+
- The underlying account.get_account_posts(sort="replies", raw_data=True) call provides the source data; when it returns None or the instance is not connected, construction exits early.
|
|
1284
|
+
"""
|
|
1285
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
1286
|
+
if not self.blockchain.is_connected():
|
|
1287
|
+
return None
|
|
1288
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
1289
|
+
account = Account(author, blockchain_instance=self.blockchain)
|
|
1290
|
+
replies = account.get_account_posts(sort="replies", raw_data=True)
|
|
1291
|
+
comments = []
|
|
1292
|
+
if replies is None:
|
|
1293
|
+
replies = []
|
|
1294
|
+
for post in replies:
|
|
1295
|
+
if skip_own and post["author"] == author:
|
|
1296
|
+
continue
|
|
1297
|
+
comments.append(
|
|
1298
|
+
Comment(post, lazy=lazy, full=full, blockchain_instance=self.blockchain)
|
|
1299
|
+
)
|
|
1300
|
+
super().__init__(comments)
|
|
1301
|
+
|
|
1302
|
+
|
|
1303
|
+
class RecentByPath(list):
|
|
1304
|
+
"""Obtain a list of posts recent by path, does the same as RankedPosts
|
|
1305
|
+
|
|
1306
|
+
:param str path: path
|
|
1307
|
+
:param str tag: tag
|
|
1308
|
+
:param str observer: observer
|
|
1309
|
+
:param Blockchain blockchain_instance: Blockchain instance to use when accessing the RPC
|
|
1310
|
+
"""
|
|
1311
|
+
|
|
1312
|
+
def __init__(
|
|
1313
|
+
self,
|
|
1314
|
+
path="trending",
|
|
1315
|
+
tag="",
|
|
1316
|
+
observer="",
|
|
1317
|
+
lazy=False,
|
|
1318
|
+
full=True,
|
|
1319
|
+
limit=20,
|
|
1320
|
+
blockchain_instance=None,
|
|
1321
|
+
):
|
|
1322
|
+
"""
|
|
1323
|
+
Create a RecentByPath list by fetching ranked posts for a given path/tag and initializing the list with those posts.
|
|
1324
|
+
|
|
1325
|
+
Parameters:
|
|
1326
|
+
path (str): Ranking category to fetch (e.g., "trending", "hot").
|
|
1327
|
+
tag (str): Optional tag to filter posts.
|
|
1328
|
+
observer (str): Observer account used for context-aware fetches (affects reward/curation visibility).
|
|
1329
|
+
lazy (bool): If True, create Comment objects lazily (defer full data loading).
|
|
1330
|
+
full (bool): If True, initialize Comment objects with full data when available.
|
|
1331
|
+
limit (int): Maximum number of posts to fetch.
|
|
1332
|
+
"""
|
|
1333
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
1334
|
+
|
|
1335
|
+
# Create RankedPosts with proper parameters
|
|
1336
|
+
ranked_posts = RankedPosts(
|
|
1337
|
+
sort=path,
|
|
1338
|
+
tag=tag,
|
|
1339
|
+
observer=observer,
|
|
1340
|
+
limit=limit,
|
|
1341
|
+
lazy=lazy,
|
|
1342
|
+
full=full,
|
|
1343
|
+
blockchain_instance=self.blockchain,
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
super().__init__(ranked_posts)
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
class RankedPosts(list):
|
|
1350
|
+
"""Obtain a list of ranked posts
|
|
1351
|
+
|
|
1352
|
+
:param str sort: can be: trending, hot, created, promoted, payout, payout_comments, muted
|
|
1353
|
+
:param str tag: tag, when used my, the community posts of the observer are shown
|
|
1354
|
+
:param str observer: Observer name
|
|
1355
|
+
:param int limit: limits the number of returns comments
|
|
1356
|
+
:param str start_author: start author
|
|
1357
|
+
:param str start_permlink: start permlink
|
|
1358
|
+
:param Blockchain blockchain_instance: Blockchain instance to use when accessing the RPC
|
|
1359
|
+
"""
|
|
1360
|
+
|
|
1361
|
+
def __init__(
|
|
1362
|
+
self,
|
|
1363
|
+
sort,
|
|
1364
|
+
tag="",
|
|
1365
|
+
observer="",
|
|
1366
|
+
limit=21,
|
|
1367
|
+
start_author="",
|
|
1368
|
+
start_permlink="",
|
|
1369
|
+
lazy=False,
|
|
1370
|
+
full=True,
|
|
1371
|
+
raw_data=False,
|
|
1372
|
+
blockchain_instance=None,
|
|
1373
|
+
):
|
|
1374
|
+
"""
|
|
1375
|
+
Initialize a RankedPosts list by fetching paginated ranked posts from the blockchain.
|
|
1376
|
+
|
|
1377
|
+
Fetches up to `limit` posts for the given `sort` and `tag` using the bridge `get_ranked_posts`
|
|
1378
|
+
RPC, paging with `start_author` / `start_permlink`. Results are appended to the list as raw
|
|
1379
|
+
post dicts when `raw_data` is True, or as Comment objects otherwise. The constructor:
|
|
1380
|
+
- uses `blockchain_instance` (or the shared instance) and returns early (None) if not connected;
|
|
1381
|
+
- pages through results with an API page size up to 100, updating `start_author`/`start_permlink`;
|
|
1382
|
+
- avoids repeating the last item returned by the API; and
|
|
1383
|
+
- on an RPC error, returns partial results if any posts were already collected, otherwise re-raises.
|
|
1384
|
+
|
|
1385
|
+
Parameters:
|
|
1386
|
+
sort (str): Ranking to query (e.g., "trending", "hot", "created").
|
|
1387
|
+
tag (str): Optional tag/category to filter by.
|
|
1388
|
+
observer (str): Optional observer account used by the bridge API (affects personalized results).
|
|
1389
|
+
limit (int): Maximum number of posts to return.
|
|
1390
|
+
start_author (str): Author to start paging from (inclusive/exclusive depends on the API).
|
|
1391
|
+
start_permlink (str): Permlink to start paging from.
|
|
1392
|
+
lazy (bool): If False, wrap results in Comment objects fully; if True, create Comment objects in lazy mode.
|
|
1393
|
+
full (bool): If True, request full Comment initialization when wrapping results.
|
|
1394
|
+
raw_data (bool): If True, return raw post dictionaries instead of Comment objects.
|
|
1395
|
+
"""
|
|
1396
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
1397
|
+
if not self.blockchain.is_connected():
|
|
1398
|
+
return None
|
|
1399
|
+
comments = []
|
|
1400
|
+
# Bridge API enforces a maximum page size (typically 20). Cap
|
|
1401
|
+
# per-request size accordingly and page until `limit` is reached.
|
|
1402
|
+
# Previously capped to 100 which can trigger Invalid parameters.
|
|
1403
|
+
api_limit = min(limit, 20)
|
|
1404
|
+
last_n = -1
|
|
1405
|
+
while len(comments) < limit and last_n != len(comments):
|
|
1406
|
+
last_n = len(comments)
|
|
1407
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1408
|
+
try:
|
|
1409
|
+
posts = self.blockchain.rpc.get_ranked_posts(
|
|
1410
|
+
{
|
|
1411
|
+
"sort": sort,
|
|
1412
|
+
"tag": tag,
|
|
1413
|
+
"observer": observer,
|
|
1414
|
+
"limit": api_limit,
|
|
1415
|
+
"start_author": start_author,
|
|
1416
|
+
"start_permlink": start_permlink,
|
|
1417
|
+
},
|
|
1418
|
+
)
|
|
1419
|
+
if posts is None:
|
|
1420
|
+
continue
|
|
1421
|
+
for post in posts:
|
|
1422
|
+
if (
|
|
1423
|
+
len(comments) > 0
|
|
1424
|
+
and comments[-1]["author"] == post["author"]
|
|
1425
|
+
and comments[-1]["permlink"] == post["permlink"]
|
|
1426
|
+
):
|
|
1427
|
+
continue
|
|
1428
|
+
if len(comments) >= limit:
|
|
1429
|
+
continue
|
|
1430
|
+
if raw_data:
|
|
1431
|
+
comments.append(post)
|
|
1432
|
+
else:
|
|
1433
|
+
comments.append(
|
|
1434
|
+
Comment(post, lazy=lazy, full=full, blockchain_instance=self.blockchain)
|
|
1435
|
+
)
|
|
1436
|
+
if len(comments) > 0:
|
|
1437
|
+
start_author = comments[-1]["author"]
|
|
1438
|
+
start_permlink = comments[-1]["permlink"]
|
|
1439
|
+
# Recompute per-request limit, capped to bridge max (20)
|
|
1440
|
+
remaining = limit - len(comments)
|
|
1441
|
+
api_limit = min(20, remaining + 1) if remaining < 20 else 20
|
|
1442
|
+
except Exception as e:
|
|
1443
|
+
# If we get an error but have some posts, return what we have
|
|
1444
|
+
if len(comments) > 0:
|
|
1445
|
+
logging.warning(f"Error in RankedPosts: {str(e)}. Returning partial results.")
|
|
1446
|
+
break
|
|
1447
|
+
# Otherwise, re-raise the exception
|
|
1448
|
+
raise
|
|
1449
|
+
super().__init__(comments)
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
class AccountPosts(list):
|
|
1453
|
+
"""Obtain a list of account related posts
|
|
1454
|
+
|
|
1455
|
+
:param str sort: can be: comments, posts, blog, replies, feed
|
|
1456
|
+
:param str account: Account name
|
|
1457
|
+
:param str observer: Observer name
|
|
1458
|
+
:param int limit: limits the number of returns comments
|
|
1459
|
+
:param str start_author: start author
|
|
1460
|
+
:param str start_permlink: start permlink
|
|
1461
|
+
:param Blockchain blockchain_instance: Blockchain instance to use when accessing the RPC
|
|
1462
|
+
"""
|
|
1463
|
+
|
|
1464
|
+
def __init__(
|
|
1465
|
+
self,
|
|
1466
|
+
sort,
|
|
1467
|
+
account,
|
|
1468
|
+
observer="",
|
|
1469
|
+
limit=20,
|
|
1470
|
+
start_author="",
|
|
1471
|
+
start_permlink="",
|
|
1472
|
+
lazy=False,
|
|
1473
|
+
full=True,
|
|
1474
|
+
raw_data=False,
|
|
1475
|
+
blockchain_instance=None,
|
|
1476
|
+
):
|
|
1477
|
+
"""
|
|
1478
|
+
Initialize an AccountPosts list by fetching posts for a given account (paginated).
|
|
1479
|
+
|
|
1480
|
+
This constructor populates the list with posts returned by the bridge.get_account_posts RPC call,
|
|
1481
|
+
respecting paging (start_author/start_permlink) and the requested limit. Each item is either the
|
|
1482
|
+
raw post dict (when raw_data=True) or a Comment object constructed with the same blockchain instance.
|
|
1483
|
+
|
|
1484
|
+
Parameters:
|
|
1485
|
+
sort (str): The post list type to fetch (e.g., "blog", "comments", "replies", "feed").
|
|
1486
|
+
account (str): Account name whose posts are requested.
|
|
1487
|
+
observer (str): Optional observer account name used by the API (affects visibility/context).
|
|
1488
|
+
limit (int): Maximum number of posts to collect.
|
|
1489
|
+
start_author (str): Author to start paging from (inclusive/exclusive depends on API).
|
|
1490
|
+
start_permlink (str): Permlink to start paging from.
|
|
1491
|
+
lazy (bool): If False, Comment objects are fully loaded; if True, they are initialized lazily.
|
|
1492
|
+
full (bool): If True, Comment objects include full data; otherwise minimal fields.
|
|
1493
|
+
raw_data (bool): If True, return raw post dicts instead of Comment objects.
|
|
1494
|
+
|
|
1495
|
+
Behavior notes:
|
|
1496
|
+
- If the blockchain instance is not connected, initialization returns early (no posts are fetched).
|
|
1497
|
+
- On an RPC error, if some posts have already been collected, the constructor returns those partial results;
|
|
1498
|
+
if no posts were collected, the exception is propagated.
|
|
1499
|
+
"""
|
|
1500
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
1501
|
+
if not self.blockchain.is_connected():
|
|
1502
|
+
return None
|
|
1503
|
+
comments = []
|
|
1504
|
+
# Bridge API typically restricts page size to 20; cap per-request size
|
|
1505
|
+
# and page as needed until overall `limit` is satisfied.
|
|
1506
|
+
api_limit = min(limit, 20)
|
|
1507
|
+
last_n = -1
|
|
1508
|
+
while len(comments) < limit and last_n != len(comments):
|
|
1509
|
+
last_n = len(comments)
|
|
1510
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1511
|
+
try:
|
|
1512
|
+
posts = self.blockchain.rpc.get_account_posts(
|
|
1513
|
+
{
|
|
1514
|
+
"sort": sort,
|
|
1515
|
+
"account": account,
|
|
1516
|
+
"observer": observer,
|
|
1517
|
+
"limit": api_limit,
|
|
1518
|
+
"start_author": start_author,
|
|
1519
|
+
"start_permlink": start_permlink,
|
|
1520
|
+
},
|
|
1521
|
+
)
|
|
1522
|
+
if posts is None:
|
|
1523
|
+
continue
|
|
1524
|
+
for post in posts:
|
|
1525
|
+
if (
|
|
1526
|
+
len(comments) > 0
|
|
1527
|
+
and comments[-1]["author"] == post["author"]
|
|
1528
|
+
and comments[-1]["permlink"] == post["permlink"]
|
|
1529
|
+
):
|
|
1530
|
+
continue
|
|
1531
|
+
if len(comments) >= limit:
|
|
1532
|
+
continue
|
|
1533
|
+
if raw_data:
|
|
1534
|
+
comments.append(post)
|
|
1535
|
+
else:
|
|
1536
|
+
comments.append(
|
|
1537
|
+
Comment(post, lazy=lazy, full=full, blockchain_instance=self.blockchain)
|
|
1538
|
+
)
|
|
1539
|
+
if len(comments) > 0:
|
|
1540
|
+
start_author = comments[-1]["author"]
|
|
1541
|
+
start_permlink = comments[-1]["permlink"]
|
|
1542
|
+
# Recompute per-request limit for next page, still capped at 20
|
|
1543
|
+
remaining = limit - len(comments)
|
|
1544
|
+
api_limit = min(20, remaining + 1) if remaining < 20 else 20
|
|
1545
|
+
except Exception as e:
|
|
1546
|
+
# If we get an error but have some posts, return what we have
|
|
1547
|
+
if len(comments) > 0:
|
|
1548
|
+
logging.warning(f"Error in AccountPosts: {str(e)}. Returning partial results.")
|
|
1549
|
+
break
|
|
1550
|
+
# Otherwise, re-raise the exception
|
|
1551
|
+
raise
|
|
1552
|
+
super().__init__(comments)
|