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/snapshot.py
ADDED
|
@@ -0,0 +1,1024 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
import warnings
|
|
5
|
+
from bisect import bisect_left
|
|
6
|
+
from datetime import date, datetime, time, timedelta, timezone
|
|
7
|
+
from typing import Any, Dict, Generator, List, Optional, Union
|
|
8
|
+
|
|
9
|
+
from nectar.account import Account
|
|
10
|
+
from nectar.amount import Amount
|
|
11
|
+
from nectar.constants import HIVE_100_PERCENT, HIVE_VOTE_REGENERATION_SECONDS
|
|
12
|
+
from nectar.instance import shared_blockchain_instance
|
|
13
|
+
from nectar.utils import (
|
|
14
|
+
addTzInfo,
|
|
15
|
+
formatTimeString,
|
|
16
|
+
parse_time,
|
|
17
|
+
reputation_to_score,
|
|
18
|
+
)
|
|
19
|
+
from nectar.vote import Vote
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AccountSnapshot(list):
|
|
25
|
+
"""This class allows to easily access Account history
|
|
26
|
+
|
|
27
|
+
:param str account_name: Name of the account
|
|
28
|
+
:param Hive blockchain_instance: Hive instance
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
account: Union[str, Account],
|
|
34
|
+
account_history: Optional[List[Dict[str, Any]]] = None,
|
|
35
|
+
blockchain_instance: Optional[Any] = None,
|
|
36
|
+
**kwargs: Any,
|
|
37
|
+
) -> None:
|
|
38
|
+
super().__init__(account_history or [])
|
|
39
|
+
# Warn about any unused kwargs to maintain backward compatibility
|
|
40
|
+
"""
|
|
41
|
+
Initialize an AccountSnapshot for the given account.
|
|
42
|
+
|
|
43
|
+
Creates an Account object for the target account (using the provided blockchain instance or the shared instance), resets internal snapshot state, and populates the snapshot list with any provided account_history. Any unexpected keyword arguments are accepted but ignored; a DeprecationWarning is emitted for each.
|
|
44
|
+
|
|
45
|
+
Parameters:
|
|
46
|
+
account (str or Account): Account identifier or existing Account object to build the snapshot for.
|
|
47
|
+
account_history (iterable, optional): Pre-fetched sequence of account operations to initialize the snapshot with. Defaults to an empty list.
|
|
48
|
+
|
|
49
|
+
Notes:
|
|
50
|
+
- blockchain_instance is accepted but not documented here as a service-like parameter; if provided it overrides the shared blockchain instance used to construct the Account.
|
|
51
|
+
- This initializer mutates internal state (calls reset) and appends account_history into the snapshot's underlying list.
|
|
52
|
+
"""
|
|
53
|
+
if kwargs:
|
|
54
|
+
for key in kwargs:
|
|
55
|
+
warnings.warn(
|
|
56
|
+
f"Unexpected keyword argument '{key}' passed to AccountSnapshot.__init__. "
|
|
57
|
+
"This may be a deprecated parameter and will be ignored.",
|
|
58
|
+
DeprecationWarning,
|
|
59
|
+
stacklevel=2,
|
|
60
|
+
)
|
|
61
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
62
|
+
self.account = Account(account, blockchain_instance=self.blockchain)
|
|
63
|
+
self.reset()
|
|
64
|
+
# super().__init__(account_history or [])
|
|
65
|
+
|
|
66
|
+
def reset(self) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Reset internal time-series and aggregation arrays while preserving the stored account history.
|
|
69
|
+
|
|
70
|
+
Reinitializes per-timestamp state used to build derived metrics (vesting, Hive/HBD balances, delegations,
|
|
71
|
+
operation statistics, reward and vote timelines, reputation and voting-power arrays). Does not modify
|
|
72
|
+
the list contents (the stored raw account history).
|
|
73
|
+
"""
|
|
74
|
+
self.own_vests = [
|
|
75
|
+
Amount(0, self.blockchain.vest_token_symbol, blockchain_instance=self.blockchain)
|
|
76
|
+
]
|
|
77
|
+
self.own_hive = [
|
|
78
|
+
Amount(0, self.blockchain.token_symbol, blockchain_instance=self.blockchain)
|
|
79
|
+
]
|
|
80
|
+
self.own_hbd = [
|
|
81
|
+
Amount(0, self.blockchain.backed_token_symbol, blockchain_instance=self.blockchain)
|
|
82
|
+
]
|
|
83
|
+
self.delegated_vests_in = [{}]
|
|
84
|
+
self.delegated_vests_out = [{}]
|
|
85
|
+
self.timestamps = [addTzInfo(datetime(1970, 1, 1, 0, 0, 0, 0))]
|
|
86
|
+
self.ops_statistics = {}
|
|
87
|
+
for key in self.blockchain.get_operation_names():
|
|
88
|
+
self.ops_statistics[key] = 0
|
|
89
|
+
self.reward_timestamps = []
|
|
90
|
+
self.author_rewards = []
|
|
91
|
+
self.curation_rewards = []
|
|
92
|
+
self.curation_per_1000_HP_timestamp = []
|
|
93
|
+
self.curation_per_1000_HP = []
|
|
94
|
+
self.out_vote_timestamp = []
|
|
95
|
+
self.out_vote_weight = []
|
|
96
|
+
self.in_vote_timestamp = []
|
|
97
|
+
self.in_vote_weight = []
|
|
98
|
+
self.in_vote_rep = []
|
|
99
|
+
self.in_vote_rshares = []
|
|
100
|
+
self.vp = []
|
|
101
|
+
self.vp_timestamp = []
|
|
102
|
+
self.downvote_vp = []
|
|
103
|
+
self.downvote_vp_timestamp = []
|
|
104
|
+
self.rep = []
|
|
105
|
+
self.rep_timestamp = []
|
|
106
|
+
|
|
107
|
+
def search(
|
|
108
|
+
self,
|
|
109
|
+
search_str: str,
|
|
110
|
+
start: Optional[Union[datetime, date, time, int]] = None,
|
|
111
|
+
stop: Optional[Union[datetime, date, time, int]] = None,
|
|
112
|
+
use_block_num: bool = True,
|
|
113
|
+
) -> List[Dict[str, Any]]:
|
|
114
|
+
"""Returns ops in the given range"""
|
|
115
|
+
ops = []
|
|
116
|
+
if start is not None and not isinstance(start, int):
|
|
117
|
+
start = addTzInfo(start)
|
|
118
|
+
if stop is not None and not isinstance(stop, int):
|
|
119
|
+
stop = addTzInfo(stop)
|
|
120
|
+
for op in self:
|
|
121
|
+
if use_block_num and start is not None and isinstance(start, int):
|
|
122
|
+
if op["block"] < start:
|
|
123
|
+
continue
|
|
124
|
+
elif not use_block_num and start is not None and isinstance(start, int):
|
|
125
|
+
if op["index"] < start:
|
|
126
|
+
continue
|
|
127
|
+
elif start is not None and isinstance(start, (datetime, date, time)):
|
|
128
|
+
start_dt = addTzInfo(start) if isinstance(start, (date, time)) else start
|
|
129
|
+
# Ensure start_dt is always datetime for comparison
|
|
130
|
+
if isinstance(start_dt, (date, time)):
|
|
131
|
+
start_dt = addTzInfo(start_dt)
|
|
132
|
+
if start_dt is None:
|
|
133
|
+
continue
|
|
134
|
+
# Convert to datetime if still not datetime
|
|
135
|
+
if isinstance(start_dt, time):
|
|
136
|
+
start_dt = datetime.combine(datetime.now().date(), start_dt)
|
|
137
|
+
elif isinstance(start_dt, date):
|
|
138
|
+
start_dt = datetime.combine(start_dt, time.min)
|
|
139
|
+
op_timestamp_dt = formatTimeString(op["timestamp"])
|
|
140
|
+
if isinstance(op_timestamp_dt, str):
|
|
141
|
+
op_timestamp_dt = parse_time(op_timestamp_dt)
|
|
142
|
+
if start_dt > op_timestamp_dt:
|
|
143
|
+
continue
|
|
144
|
+
if use_block_num and stop is not None and isinstance(stop, int):
|
|
145
|
+
if op["block"] > stop:
|
|
146
|
+
continue
|
|
147
|
+
elif not use_block_num and stop is not None and isinstance(stop, int):
|
|
148
|
+
if op["index"] > stop:
|
|
149
|
+
continue
|
|
150
|
+
elif stop is not None and isinstance(stop, (datetime, date, time)):
|
|
151
|
+
stop_dt = addTzInfo(stop) if isinstance(stop, (date, time)) else stop
|
|
152
|
+
# Ensure stop_dt is always datetime for comparison
|
|
153
|
+
if isinstance(stop_dt, (date, time)):
|
|
154
|
+
stop_dt = addTzInfo(stop_dt)
|
|
155
|
+
if stop_dt is None:
|
|
156
|
+
continue
|
|
157
|
+
# Convert to datetime if still not datetime
|
|
158
|
+
if isinstance(stop_dt, time):
|
|
159
|
+
stop_dt = datetime.combine(datetime.now().date(), stop_dt)
|
|
160
|
+
elif isinstance(stop_dt, date):
|
|
161
|
+
stop_dt = datetime.combine(stop_dt, time.min)
|
|
162
|
+
op_timestamp_dt = formatTimeString(op["timestamp"])
|
|
163
|
+
if isinstance(op_timestamp_dt, str):
|
|
164
|
+
op_timestamp_dt = parse_time(op_timestamp_dt)
|
|
165
|
+
if stop_dt < op_timestamp_dt:
|
|
166
|
+
continue
|
|
167
|
+
op_string = json.dumps(list(op.values()))
|
|
168
|
+
if re.search(search_str, op_string):
|
|
169
|
+
ops.append(op)
|
|
170
|
+
return ops
|
|
171
|
+
|
|
172
|
+
def get_ops(
|
|
173
|
+
self,
|
|
174
|
+
start: Optional[Union[datetime, date, time, int]] = None,
|
|
175
|
+
stop: Optional[Union[datetime, date, time, int]] = None,
|
|
176
|
+
use_block_num: bool = True,
|
|
177
|
+
only_ops: Optional[List[str]] = None,
|
|
178
|
+
exclude_ops: Optional[List[str]] = None,
|
|
179
|
+
) -> Generator[Dict[str, Any], None, None]:
|
|
180
|
+
"""Returns ops in the given range"""
|
|
181
|
+
if only_ops is None:
|
|
182
|
+
only_ops = []
|
|
183
|
+
if exclude_ops is None:
|
|
184
|
+
exclude_ops = []
|
|
185
|
+
if start is not None and not isinstance(start, int):
|
|
186
|
+
start = addTzInfo(start)
|
|
187
|
+
if stop is not None and not isinstance(stop, int):
|
|
188
|
+
stop = addTzInfo(stop)
|
|
189
|
+
for op in self:
|
|
190
|
+
if use_block_num and start is not None and isinstance(start, int):
|
|
191
|
+
if op["block"] < start:
|
|
192
|
+
continue
|
|
193
|
+
elif not use_block_num and start is not None and isinstance(start, int):
|
|
194
|
+
if op["index"] < start:
|
|
195
|
+
continue
|
|
196
|
+
elif start is not None and isinstance(start, (datetime, date, time)):
|
|
197
|
+
start_dt = addTzInfo(start) if isinstance(start, (date, time)) else start
|
|
198
|
+
# Ensure start_dt is always datetime for comparison
|
|
199
|
+
if isinstance(start_dt, (date, time)):
|
|
200
|
+
start_dt = addTzInfo(start_dt)
|
|
201
|
+
if start_dt is None:
|
|
202
|
+
continue
|
|
203
|
+
# Convert to datetime if still not datetime
|
|
204
|
+
if isinstance(start_dt, time):
|
|
205
|
+
start_dt = datetime.combine(datetime.now().date(), start_dt)
|
|
206
|
+
elif isinstance(start_dt, date):
|
|
207
|
+
start_dt = datetime.combine(start_dt, time.min)
|
|
208
|
+
op_timestamp_dt = formatTimeString(op["timestamp"])
|
|
209
|
+
if isinstance(op_timestamp_dt, str):
|
|
210
|
+
op_timestamp_dt = parse_time(op_timestamp_dt)
|
|
211
|
+
if start_dt > op_timestamp_dt:
|
|
212
|
+
continue
|
|
213
|
+
if use_block_num and stop is not None and isinstance(stop, int):
|
|
214
|
+
if op["block"] > stop:
|
|
215
|
+
continue
|
|
216
|
+
elif not use_block_num and stop is not None and isinstance(stop, int):
|
|
217
|
+
if op["index"] > stop:
|
|
218
|
+
continue
|
|
219
|
+
elif stop is not None and isinstance(stop, (datetime, date, time)):
|
|
220
|
+
stop_dt = addTzInfo(stop) if isinstance(stop, (date, time)) else stop
|
|
221
|
+
# Ensure stop_dt is always datetime for comparison
|
|
222
|
+
if isinstance(stop_dt, (date, time)):
|
|
223
|
+
stop_dt = addTzInfo(stop_dt)
|
|
224
|
+
if stop_dt is None:
|
|
225
|
+
continue
|
|
226
|
+
# Convert to datetime if still not datetime
|
|
227
|
+
if isinstance(stop_dt, time):
|
|
228
|
+
stop_dt = datetime.combine(datetime.now().date(), stop_dt)
|
|
229
|
+
elif isinstance(stop_dt, date):
|
|
230
|
+
stop_dt = datetime.combine(stop_dt, time.min)
|
|
231
|
+
op_timestamp_dt = formatTimeString(op["timestamp"])
|
|
232
|
+
if isinstance(op_timestamp_dt, str):
|
|
233
|
+
op_timestamp_dt = parse_time(op_timestamp_dt)
|
|
234
|
+
if stop_dt < op_timestamp_dt:
|
|
235
|
+
continue
|
|
236
|
+
if exclude_ops and op["type"] in exclude_ops:
|
|
237
|
+
continue
|
|
238
|
+
if not only_ops or op["type"] in only_ops:
|
|
239
|
+
yield op
|
|
240
|
+
|
|
241
|
+
def get_data(
|
|
242
|
+
self, timestamp: Optional[Union[datetime, date, time]] = None, index: int = 0
|
|
243
|
+
) -> Dict[str, Any]:
|
|
244
|
+
"""
|
|
245
|
+
Return a dictionary snapshot of the account state at or immediately before the given timestamp.
|
|
246
|
+
|
|
247
|
+
If timestamp is None the current UTC time is used. The timestamp is normalized to a timezone-aware UTC value. The method finds the most recent stored tick whose timestamp is <= the requested time and returns a dict with the corresponding state:
|
|
248
|
+
- "timestamp": stored timestamp used
|
|
249
|
+
- "vests": own vesting shares at that tick
|
|
250
|
+
- "delegated_vests_in": mapping of incoming delegations at that tick
|
|
251
|
+
- "delegated_vests_out": mapping of outgoing delegations at that tick
|
|
252
|
+
- "sp_own": Hive Power equivalent of own vests at that tick
|
|
253
|
+
- "sp_eff": effective Hive Power (own + delegated_in - delegated_out) at that tick
|
|
254
|
+
- "hive": liquid HIVE balance at that tick
|
|
255
|
+
- "hbd": liquid HBD balance at that tick
|
|
256
|
+
- "index": index into the internal arrays for that tick
|
|
257
|
+
|
|
258
|
+
Returns an empty dict if the requested timestamp is earlier than the earliest stored timestamp.
|
|
259
|
+
"""
|
|
260
|
+
if timestamp is None:
|
|
261
|
+
timestamp = datetime.now(timezone.utc)
|
|
262
|
+
timestamp = addTzInfo(timestamp)
|
|
263
|
+
# Ensure timestamp is datetime for bisect_left
|
|
264
|
+
if timestamp is None:
|
|
265
|
+
timestamp = datetime.now(timezone.utc)
|
|
266
|
+
elif isinstance(timestamp, (date, time)):
|
|
267
|
+
timestamp = addTzInfo(timestamp)
|
|
268
|
+
if timestamp is None:
|
|
269
|
+
timestamp = datetime.now(timezone.utc)
|
|
270
|
+
# Find rightmost value less than x
|
|
271
|
+
i = bisect_left(self.timestamps, timestamp)
|
|
272
|
+
if i:
|
|
273
|
+
index = i - 1
|
|
274
|
+
else:
|
|
275
|
+
return {}
|
|
276
|
+
ts = self.timestamps[index]
|
|
277
|
+
own = self.own_vests[index]
|
|
278
|
+
din = self.delegated_vests_in[index]
|
|
279
|
+
dout = self.delegated_vests_out[index]
|
|
280
|
+
hive = self.own_hive[index]
|
|
281
|
+
hbd = self.own_hbd[index]
|
|
282
|
+
sum_in = sum([din[key].amount for key in din])
|
|
283
|
+
sum_out = sum([dout[key].amount for key in dout])
|
|
284
|
+
sp_in = self.blockchain.vests_to_hp(sum_in, timestamp=ts)
|
|
285
|
+
sp_out = self.blockchain.vests_to_hp(sum_out, timestamp=ts)
|
|
286
|
+
sp_own = self.blockchain.vests_to_hp(own, timestamp=ts)
|
|
287
|
+
sp_eff = sp_own + sp_in - sp_out
|
|
288
|
+
return {
|
|
289
|
+
"timestamp": ts,
|
|
290
|
+
"vests": own,
|
|
291
|
+
"delegated_vests_in": din,
|
|
292
|
+
"delegated_vests_out": dout,
|
|
293
|
+
"sp_own": sp_own,
|
|
294
|
+
"sp_eff": sp_eff,
|
|
295
|
+
"hive": hive,
|
|
296
|
+
"hbd": hbd,
|
|
297
|
+
"index": index,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
def get_account_history(
|
|
301
|
+
self,
|
|
302
|
+
start: Optional[Union[datetime, date, time, int]] = None,
|
|
303
|
+
stop: Optional[Union[datetime, date, time, int]] = None,
|
|
304
|
+
use_block_num: bool = True,
|
|
305
|
+
) -> None:
|
|
306
|
+
"""
|
|
307
|
+
Populate the snapshot with the account's history between start and stop.
|
|
308
|
+
|
|
309
|
+
Fetches operations from the underlying Account.history iterator and replaces the snapshot's contents with those operations. If start/stop are provided they may be block numbers or datetimes; set use_block_num=False to interpret them as virtual operation indices/timestamps instead of block numbers.
|
|
310
|
+
"""
|
|
311
|
+
self.clear()
|
|
312
|
+
self.extend(
|
|
313
|
+
h for h in self.account.history(start=start, stop=stop, use_block_num=use_block_num)
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def update_rewards(
|
|
317
|
+
self,
|
|
318
|
+
timestamp: Union[datetime, int],
|
|
319
|
+
curation_reward: Union[Amount, float],
|
|
320
|
+
author_vests: Union[Amount, float],
|
|
321
|
+
author_hive: Union[Amount, float],
|
|
322
|
+
author_hbd: Union[Amount, float],
|
|
323
|
+
) -> None:
|
|
324
|
+
"""
|
|
325
|
+
Record a reward event at a given timestamp.
|
|
326
|
+
|
|
327
|
+
Appends the reward timestamp, the curation portion, and the author's reward components (vests, hive, hbd)
|
|
328
|
+
to the snapshot's internal reward arrays so they can be used by later aggregation and analysis.
|
|
329
|
+
|
|
330
|
+
Parameters:
|
|
331
|
+
timestamp (datetime or int): Event time (timezone-aware datetime or block/time integer) for the reward.
|
|
332
|
+
curation_reward (Amount or number): Curation reward amount (in vests or numeric representation used by the codebase).
|
|
333
|
+
author_vests (Amount or number): Author reward in vesting shares.
|
|
334
|
+
author_hive (Amount or number): Author reward in liquid HIVE.
|
|
335
|
+
author_hbd (Amount or number): Author reward in HBD.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
None
|
|
339
|
+
"""
|
|
340
|
+
self.reward_timestamps.append(timestamp)
|
|
341
|
+
self.curation_rewards.append(curation_reward)
|
|
342
|
+
self.author_rewards.append({"vests": author_vests, "hive": author_hive, "hbd": author_hbd})
|
|
343
|
+
|
|
344
|
+
def update_out_vote(self, timestamp: Union[datetime, int], weight: int) -> None:
|
|
345
|
+
"""
|
|
346
|
+
Record an outbound vote event.
|
|
347
|
+
|
|
348
|
+
Appends the vote timestamp and weight to the snapshot's outbound-vote arrays for later voting-power and history calculations.
|
|
349
|
+
|
|
350
|
+
Parameters:
|
|
351
|
+
timestamp (datetime | int): Time of the vote (timezone-aware datetime or block timestamp).
|
|
352
|
+
weight (int): Vote weight as an integer (e.g., range -10000..10000).
|
|
353
|
+
"""
|
|
354
|
+
self.out_vote_timestamp.append(timestamp)
|
|
355
|
+
self.out_vote_weight.append(weight)
|
|
356
|
+
|
|
357
|
+
def update_in_vote(
|
|
358
|
+
self, timestamp: Union[datetime, int], weight: int, op: Dict[str, Any]
|
|
359
|
+
) -> None:
|
|
360
|
+
"""
|
|
361
|
+
Record an incoming vote event by parsing a Vote operation and appending its data to the snapshot's in-vote arrays.
|
|
362
|
+
|
|
363
|
+
Parses the provided operation into a Vote, refreshes it, and on success appends:
|
|
364
|
+
- timestamp to in_vote_timestamp
|
|
365
|
+
- weight to in_vote_weight
|
|
366
|
+
- the voter's reputation to in_vote_rep (as int)
|
|
367
|
+
- the vote's rshares to in_vote_rshares (as int)
|
|
368
|
+
|
|
369
|
+
Parameters:
|
|
370
|
+
timestamp: datetime
|
|
371
|
+
Time of the vote event (should be timezone-aware).
|
|
372
|
+
weight: int
|
|
373
|
+
Vote weight as provided by the operation.
|
|
374
|
+
op:
|
|
375
|
+
Raw operation data used to construct the Vote.
|
|
376
|
+
|
|
377
|
+
Notes:
|
|
378
|
+
If the operation cannot be parsed into a valid Vote, the function prints an error message and returns without modifying the snapshot.
|
|
379
|
+
"""
|
|
380
|
+
v = Vote(op)
|
|
381
|
+
try:
|
|
382
|
+
v.refresh()
|
|
383
|
+
self.in_vote_timestamp.append(timestamp)
|
|
384
|
+
self.in_vote_weight.append(weight)
|
|
385
|
+
self.in_vote_rep.append(int(v["reputation"]))
|
|
386
|
+
self.in_vote_rshares.append(int(v["rshares"]))
|
|
387
|
+
except Exception:
|
|
388
|
+
print("Could not find: %s" % v)
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
def update(
|
|
392
|
+
self,
|
|
393
|
+
timestamp: datetime,
|
|
394
|
+
own: Union[Amount, float],
|
|
395
|
+
delegated_in: Optional[Union[Dict[str, Any], int]] = None,
|
|
396
|
+
delegated_out: Optional[Union[Dict[str, Any], int]] = None,
|
|
397
|
+
hive: Union[Amount, float] = 0,
|
|
398
|
+
hbd: Union[Amount, float] = 0,
|
|
399
|
+
) -> None:
|
|
400
|
+
"""
|
|
401
|
+
Update internal time-series state with a new account event.
|
|
402
|
+
|
|
403
|
+
Appends two timeline entries: a one-second "pre-tick" preserving the previous state at timestamp - 1s, then a tick at timestamp with updated balances and delegation maps. This updates the snapshot's arrays for timestamps, own vests, liquid HIVE/HBD balances, and incoming/outgoing vesting delegations.
|
|
404
|
+
|
|
405
|
+
Parameters:
|
|
406
|
+
timestamp (datetime): Event time (timezone-aware). A "pre-tick" is created at timestamp - 1s.
|
|
407
|
+
own (Amount or float): Change in the account's own vesting shares (vests) to apply at the tick.
|
|
408
|
+
delegated_in (dict or None): Incoming delegation change of form {"account": name, "amount": vests} or None.
|
|
409
|
+
If amount == 0 the delegation entry for that account is removed.
|
|
410
|
+
delegated_out (dict or None): Outgoing delegation change. Typical forms:
|
|
411
|
+
- {"account": name, "amount": vests} to add/update a non-zero outgoing delegation.
|
|
412
|
+
- {"account": None, "amount": vests} indicates a return_vesting_delegation; the matching outgoing entry with the same amount is removed.
|
|
413
|
+
If omitted or empty, outgoing delegations are unchanged.
|
|
414
|
+
hive (Amount or float): Change in liquid HIVE to apply at the tick.
|
|
415
|
+
hbd (Amount or float): Change in liquid HBD to apply at the tick.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
None
|
|
419
|
+
"""
|
|
420
|
+
self.timestamps.append(timestamp - timedelta(seconds=1))
|
|
421
|
+
self.own_vests.append(self.own_vests[-1])
|
|
422
|
+
self.own_hive.append(self.own_hive[-1])
|
|
423
|
+
self.own_hbd.append(self.own_hbd[-1])
|
|
424
|
+
self.delegated_vests_in.append(self.delegated_vests_in[-1])
|
|
425
|
+
self.delegated_vests_out.append(self.delegated_vests_out[-1])
|
|
426
|
+
|
|
427
|
+
self.timestamps.append(timestamp)
|
|
428
|
+
self.own_vests.append(self.own_vests[-1] + own)
|
|
429
|
+
self.own_hive.append(self.own_hive[-1] + hive)
|
|
430
|
+
self.own_hbd.append(self.own_hbd[-1] + hbd)
|
|
431
|
+
|
|
432
|
+
new_deleg = dict(self.delegated_vests_in[-1])
|
|
433
|
+
if delegated_in is not None and delegated_in:
|
|
434
|
+
if delegated_in["amount"] == 0:
|
|
435
|
+
del new_deleg[delegated_in["account"]]
|
|
436
|
+
else:
|
|
437
|
+
new_deleg[delegated_in["account"]] = delegated_in["amount"]
|
|
438
|
+
self.delegated_vests_in.append(new_deleg)
|
|
439
|
+
|
|
440
|
+
new_deleg = dict(self.delegated_vests_out[-1])
|
|
441
|
+
if delegated_out is not None and delegated_out:
|
|
442
|
+
if delegated_out["account"] is None:
|
|
443
|
+
# return_vesting_delegation
|
|
444
|
+
for delegatee in new_deleg:
|
|
445
|
+
if new_deleg[delegatee]["amount"] == delegated_out["amount"]:
|
|
446
|
+
del new_deleg[delegatee]
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
elif delegated_out["amount"] != 0:
|
|
450
|
+
# new or updated non-zero delegation
|
|
451
|
+
new_deleg[delegated_out["account"]] = delegated_out["amount"]
|
|
452
|
+
# TODO
|
|
453
|
+
# skip undelegations here, wait for 'return_vesting_delegation'
|
|
454
|
+
# del new_deleg[delegated_out['account']]
|
|
455
|
+
|
|
456
|
+
self.delegated_vests_out.append(new_deleg)
|
|
457
|
+
|
|
458
|
+
def build(
|
|
459
|
+
self,
|
|
460
|
+
only_ops: Optional[List[str]] = None,
|
|
461
|
+
exclude_ops: Optional[List[str]] = None,
|
|
462
|
+
enable_rewards: bool = False,
|
|
463
|
+
enable_out_votes: bool = False,
|
|
464
|
+
enable_in_votes: bool = False,
|
|
465
|
+
) -> None:
|
|
466
|
+
"""Builds the account history based on all account operations
|
|
467
|
+
|
|
468
|
+
:param array only_ops: Limit generator by these
|
|
469
|
+
operations (*optional*)
|
|
470
|
+
:param array exclude_ops: Exclude these operations from
|
|
471
|
+
generator (*optional*)
|
|
472
|
+
|
|
473
|
+
"""
|
|
474
|
+
if only_ops is None:
|
|
475
|
+
only_ops = []
|
|
476
|
+
if exclude_ops is None:
|
|
477
|
+
exclude_ops = []
|
|
478
|
+
if len(self.timestamps) > 0:
|
|
479
|
+
start_timestamp = self.timestamps[-1]
|
|
480
|
+
else:
|
|
481
|
+
start_timestamp = None
|
|
482
|
+
for op in sorted(self, key=lambda k: k["timestamp"]):
|
|
483
|
+
ts = parse_time(op["timestamp"])
|
|
484
|
+
if start_timestamp is not None:
|
|
485
|
+
# Convert start_timestamp to datetime if it's time or date
|
|
486
|
+
if isinstance(start_timestamp, time):
|
|
487
|
+
start_timestamp_dt = datetime.combine(datetime.now().date(), start_timestamp)
|
|
488
|
+
elif isinstance(start_timestamp, date):
|
|
489
|
+
start_timestamp_dt = datetime.combine(start_timestamp, time.min)
|
|
490
|
+
else:
|
|
491
|
+
start_timestamp_dt = start_timestamp
|
|
492
|
+
if start_timestamp_dt > ts:
|
|
493
|
+
continue
|
|
494
|
+
if op["type"] in exclude_ops:
|
|
495
|
+
continue
|
|
496
|
+
if len(only_ops) > 0 and op["type"] not in only_ops:
|
|
497
|
+
continue
|
|
498
|
+
self.ops_statistics[op["type"]] += 1
|
|
499
|
+
self.parse_op(
|
|
500
|
+
op,
|
|
501
|
+
only_ops=only_ops,
|
|
502
|
+
enable_rewards=enable_rewards,
|
|
503
|
+
enable_out_votes=enable_out_votes,
|
|
504
|
+
enable_in_votes=enable_in_votes,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
def parse_op(
|
|
508
|
+
self,
|
|
509
|
+
op: Dict[str, Any],
|
|
510
|
+
only_ops: Optional[List[str]] = None,
|
|
511
|
+
enable_rewards: bool = False,
|
|
512
|
+
enable_out_votes: bool = False,
|
|
513
|
+
enable_in_votes: bool = False,
|
|
514
|
+
) -> None:
|
|
515
|
+
"""
|
|
516
|
+
Parse a single account-history operation and update the snapshot's internal state.
|
|
517
|
+
|
|
518
|
+
Parses the provided operation dictionary `op` (expects keys like "type" and "timestamp"), converts amounts using the snapshot's blockchain instance, and applies its effect to the snapshot by calling the appropriate update methods (e.g. update, update_rewards, update_out_vote, update_in_vote). Handles Hive-specific operations such as account creation, transfers, vesting/delegation, reward payouts, order fills, conversions, interest, votes, and hardfork-related adjustments. Many other operation types are intentionally ignored.
|
|
519
|
+
|
|
520
|
+
Parameters:
|
|
521
|
+
op (dict): A single operation entry from account history. Must contain a parsable "timestamp" and a "type" key; other required keys depend on the operation type.
|
|
522
|
+
only_ops (list): If non-empty, treat operations listed here as affecting balances/votes even when reward/vote-collection flags are disabled.
|
|
523
|
+
enable_rewards (bool): When True, record reward aggregates via update_rewards in addition to applying balance changes.
|
|
524
|
+
enable_out_votes (bool): When True, record outbound votes (this account as voter) via update_out_vote.
|
|
525
|
+
enable_in_votes (bool): When True, record inbound votes (this account as author/recipient) via update_in_vote.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
None
|
|
529
|
+
"""
|
|
530
|
+
if only_ops is None:
|
|
531
|
+
only_ops = []
|
|
532
|
+
ts = parse_time(op["timestamp"])
|
|
533
|
+
|
|
534
|
+
if op["type"] == "account_create":
|
|
535
|
+
fee_hive = Amount(op["fee"], blockchain_instance=self.blockchain).amount
|
|
536
|
+
fee_vests = self.blockchain.hp_to_vests(
|
|
537
|
+
Amount(op["fee"], blockchain_instance=self.blockchain).amount, timestamp=ts
|
|
538
|
+
)
|
|
539
|
+
if op["new_account_name"] == self.account["name"]:
|
|
540
|
+
self.update(ts, fee_vests, 0, 0)
|
|
541
|
+
return
|
|
542
|
+
if op["creator"] == self.account["name"]:
|
|
543
|
+
self.update(ts, 0, 0, 0, fee_hive * (-1), 0)
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
if op["type"] == "account_create_with_delegation":
|
|
547
|
+
fee_hive = Amount(op["fee"], blockchain_instance=self.blockchain).amount
|
|
548
|
+
fee_vests = self.blockchain.hp_to_vests(
|
|
549
|
+
Amount(op["fee"], blockchain_instance=self.blockchain).amount, timestamp=ts
|
|
550
|
+
)
|
|
551
|
+
if op["new_account_name"] == self.account["name"]:
|
|
552
|
+
if Amount(op["delegation"], blockchain_instance=self.blockchain).amount > 0:
|
|
553
|
+
delegation = {
|
|
554
|
+
"account": op["creator"],
|
|
555
|
+
"amount": Amount(op["delegation"], blockchain_instance=self.blockchain),
|
|
556
|
+
}
|
|
557
|
+
else:
|
|
558
|
+
delegation = None
|
|
559
|
+
self.update(ts, fee_vests, delegation, 0)
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
if op["creator"] == self.account["name"]:
|
|
563
|
+
delegation = {
|
|
564
|
+
"account": op["new_account_name"],
|
|
565
|
+
"amount": Amount(op["delegation"], blockchain_instance=self.blockchain),
|
|
566
|
+
}
|
|
567
|
+
self.update(ts, 0, 0, delegation, fee_hive * (-1), 0)
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
elif op["type"] == "delegate_vesting_shares":
|
|
571
|
+
vests = Amount(op["vesting_shares"], blockchain_instance=self.blockchain)
|
|
572
|
+
if op["delegator"] == self.account["name"]:
|
|
573
|
+
delegation = {"account": op["delegatee"], "amount": vests}
|
|
574
|
+
self.update(ts, 0, 0, delegation)
|
|
575
|
+
return
|
|
576
|
+
if op["delegatee"] == self.account["name"]:
|
|
577
|
+
delegation = {"account": op["delegator"], "amount": vests}
|
|
578
|
+
self.update(ts, 0, delegation, 0)
|
|
579
|
+
return
|
|
580
|
+
|
|
581
|
+
elif op["type"] == "transfer":
|
|
582
|
+
amount = Amount(op["amount"], blockchain_instance=self.blockchain)
|
|
583
|
+
if op["from"] == self.account["name"]:
|
|
584
|
+
if amount.symbol == self.blockchain.blockchain_symbol:
|
|
585
|
+
self.update(ts, 0, None, None, hive=amount * (-1), hbd=0)
|
|
586
|
+
elif amount.symbol == self.blockchain.backed_token_symbol:
|
|
587
|
+
self.update(ts, 0, None, None, hive=0, hbd=amount * (-1))
|
|
588
|
+
if op["to"] == self.account["name"]:
|
|
589
|
+
if amount.symbol == self.blockchain.blockchain_symbol:
|
|
590
|
+
self.update(ts, 0, None, None, hive=amount, hbd=0)
|
|
591
|
+
elif amount.symbol == self.blockchain.backed_token_symbol:
|
|
592
|
+
self.update(ts, 0, None, None, hive=0, hbd=amount)
|
|
593
|
+
return
|
|
594
|
+
|
|
595
|
+
elif op["type"] == "fill_order":
|
|
596
|
+
current_pays = Amount(op["current_pays"], blockchain_instance=self.blockchain)
|
|
597
|
+
open_pays = Amount(op["open_pays"], blockchain_instance=self.blockchain)
|
|
598
|
+
if op["current_owner"] == self.account["name"]:
|
|
599
|
+
if current_pays.symbol == self.blockchain.token_symbol:
|
|
600
|
+
self.update(ts, 0, None, None, hive=current_pays * (-1), hbd=open_pays)
|
|
601
|
+
elif current_pays.symbol == self.blockchain.backed_token_symbol:
|
|
602
|
+
self.update(ts, 0, None, None, hive=open_pays, hbd=current_pays * (-1))
|
|
603
|
+
if op["open_owner"] == self.account["name"]:
|
|
604
|
+
if current_pays.symbol == self.blockchain.token_symbol:
|
|
605
|
+
self.update(ts, 0, None, None, hive=current_pays, hbd=open_pays * (-1))
|
|
606
|
+
elif current_pays.symbol == self.blockchain.backed_token_symbol:
|
|
607
|
+
self.update(ts, 0, None, None, hive=open_pays * (-1), hbd=current_pays)
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
elif op["type"] == "transfer_to_vesting":
|
|
611
|
+
hive_amt = Amount(op["amount"], blockchain_instance=self.blockchain)
|
|
612
|
+
vests = self.blockchain.hp_to_vests(hive_amt.amount, timestamp=ts)
|
|
613
|
+
if op["from"] == self.account["name"] and op["to"] == self.account["name"]:
|
|
614
|
+
self.update(
|
|
615
|
+
ts, vests, 0, 0, hive_amt * (-1), 0
|
|
616
|
+
) # power up from and to given account
|
|
617
|
+
elif op["from"] != self.account["name"] and op["to"] == self.account["name"]:
|
|
618
|
+
self.update(ts, vests, 0, 0, 0, 0) # power up from another account
|
|
619
|
+
else: # op['from'] == self.account["name"] and op['to'] != self.account["name"]
|
|
620
|
+
self.update(ts, 0, 0, 0, hive_amt * (-1), 0) # power up to another account
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
elif op["type"] == "fill_vesting_withdraw":
|
|
624
|
+
vests = Amount(op["withdrawn"], blockchain_instance=self.blockchain)
|
|
625
|
+
self.update(ts, vests * (-1), None, None, hive=0, hbd=0)
|
|
626
|
+
return
|
|
627
|
+
|
|
628
|
+
elif op["type"] == "return_vesting_delegation":
|
|
629
|
+
delegation = {
|
|
630
|
+
"account": None,
|
|
631
|
+
"amount": Amount(op["vesting_shares"], blockchain_instance=self.blockchain),
|
|
632
|
+
}
|
|
633
|
+
self.update(ts, 0, None, delegated_out=delegation)
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
elif op["type"] == "claim_reward_balance":
|
|
637
|
+
vests = Amount(op["reward_vests"], blockchain_instance=self.blockchain)
|
|
638
|
+
hive = Amount(op["reward_hive"], blockchain_instance=self.blockchain)
|
|
639
|
+
hbd = Amount(op["reward_hbd"], blockchain_instance=self.blockchain)
|
|
640
|
+
self.update(ts, vests, None, None, hive=hive, hbd=hbd)
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
elif op["type"] == "curation_reward":
|
|
644
|
+
if "curation_reward" in only_ops or enable_rewards:
|
|
645
|
+
vests = Amount(op["reward"], blockchain_instance=self.blockchain)
|
|
646
|
+
if "curation_reward" in only_ops:
|
|
647
|
+
self.update(ts, vests, None, None, hive=0, hbd=0)
|
|
648
|
+
if enable_rewards:
|
|
649
|
+
self.update_rewards(ts, vests, 0, 0, 0)
|
|
650
|
+
return
|
|
651
|
+
|
|
652
|
+
elif op["type"] == "author_reward":
|
|
653
|
+
if "author_reward" in only_ops or enable_rewards:
|
|
654
|
+
vests = Amount(op["vesting_payout"], blockchain_instance=self.blockchain)
|
|
655
|
+
hive = Amount(op["hive_payout"], blockchain_instance=self.blockchain)
|
|
656
|
+
hbd = Amount(op["hbd_payout"], blockchain_instance=self.blockchain)
|
|
657
|
+
if "author_reward" in only_ops:
|
|
658
|
+
self.update(ts, vests, None, None, hive=hive, hbd=hbd)
|
|
659
|
+
if enable_rewards:
|
|
660
|
+
self.update_rewards(ts, 0, vests, hive, hbd)
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
elif op["type"] == "producer_reward":
|
|
664
|
+
vests = Amount(op["vesting_shares"], blockchain_instance=self.blockchain)
|
|
665
|
+
self.update(ts, vests, None, None, hive=0, hbd=0)
|
|
666
|
+
return
|
|
667
|
+
|
|
668
|
+
elif op["type"] == "comment_benefactor_reward":
|
|
669
|
+
if op["benefactor"] == self.account["name"]:
|
|
670
|
+
if "reward" in op:
|
|
671
|
+
vests = Amount(op["reward"], blockchain_instance=self.blockchain)
|
|
672
|
+
self.update(ts, vests, None, None, hive=0, hbd=0)
|
|
673
|
+
else:
|
|
674
|
+
vests = Amount(op["vesting_payout"], blockchain_instance=self.blockchain)
|
|
675
|
+
hive = Amount(op["hive_payout"], blockchain_instance=self.blockchain)
|
|
676
|
+
hbd = Amount(op["hbd_payout"], blockchain_instance=self.blockchain)
|
|
677
|
+
self.update(ts, vests, None, None, hive=hive, hbd=hbd)
|
|
678
|
+
return
|
|
679
|
+
else:
|
|
680
|
+
return
|
|
681
|
+
|
|
682
|
+
elif op["type"] == "fill_convert_request":
|
|
683
|
+
amount_in = Amount(op["amount_in"], blockchain_instance=self.blockchain)
|
|
684
|
+
amount_out = Amount(op["amount_out"], blockchain_instance=self.blockchain)
|
|
685
|
+
if op["owner"] == self.account["name"]:
|
|
686
|
+
self.update(ts, 0, None, None, hive=amount_out, hbd=amount_in * (-1))
|
|
687
|
+
return
|
|
688
|
+
|
|
689
|
+
elif op["type"] == "interest":
|
|
690
|
+
interest = Amount(op["interest"], blockchain_instance=self.blockchain)
|
|
691
|
+
self.update(ts, 0, None, None, hive=0, hbd=interest)
|
|
692
|
+
return
|
|
693
|
+
|
|
694
|
+
elif op["type"] == "vote":
|
|
695
|
+
if "vote" in only_ops or enable_out_votes:
|
|
696
|
+
weight = int(op["weight"])
|
|
697
|
+
if op["voter"] == self.account["name"]:
|
|
698
|
+
self.update_out_vote(ts, weight)
|
|
699
|
+
if "vote" in only_ops or enable_in_votes and op["author"] == self.account["name"]:
|
|
700
|
+
weight = int(op["weight"])
|
|
701
|
+
self.update_in_vote(ts, weight, op)
|
|
702
|
+
return
|
|
703
|
+
|
|
704
|
+
elif op["type"] == "hardfork_hive":
|
|
705
|
+
vests = Amount(op["vests_converted"])
|
|
706
|
+
hbd = Amount(op["hbd_transferred"])
|
|
707
|
+
hive = Amount(op["hive_transferred"])
|
|
708
|
+
self.update(ts, vests * (-1), None, None, hive=hive * (-1), hbd=hbd * (-1))
|
|
709
|
+
|
|
710
|
+
elif op["type"] in [
|
|
711
|
+
"comment",
|
|
712
|
+
"feed_publish",
|
|
713
|
+
"shutdown_witness",
|
|
714
|
+
"account_witness_vote",
|
|
715
|
+
"witness_update",
|
|
716
|
+
"custom_json",
|
|
717
|
+
"limit_order_create",
|
|
718
|
+
"account_update",
|
|
719
|
+
"account_witness_proxy",
|
|
720
|
+
"limit_order_cancel",
|
|
721
|
+
"comment_options",
|
|
722
|
+
"delete_comment",
|
|
723
|
+
"interest",
|
|
724
|
+
"recover_account",
|
|
725
|
+
"pow",
|
|
726
|
+
"fill_convert_request",
|
|
727
|
+
"convert",
|
|
728
|
+
"request_account_recovery",
|
|
729
|
+
"update_proposal_votes",
|
|
730
|
+
]:
|
|
731
|
+
return
|
|
732
|
+
|
|
733
|
+
def build_sp_arrays(self) -> None:
|
|
734
|
+
"""
|
|
735
|
+
Build timelines of own and effective Hive Power (HP) for each stored timestamp.
|
|
736
|
+
|
|
737
|
+
For every timestamp in the snapshot, convert the account's own vesting shares and the
|
|
738
|
+
sum of delegated-in/out vesting shares to Hive Power via the blockchain's
|
|
739
|
+
`vests_to_hp` conversion and populate:
|
|
740
|
+
- self.own_sp: HP equivalent of the account's own vesting shares at each timestamp.
|
|
741
|
+
- self.eff_sp: effective HP = own HP + HP delegated in - HP delegated out at each timestamp.
|
|
742
|
+
|
|
743
|
+
This method mutates self.own_sp and self.eff_sp in-place and relies on
|
|
744
|
+
self.timestamps, self.own_vests, self.delegated_vests_in, self.delegated_vests_out,
|
|
745
|
+
and self.blockchain.vests_to_hp(timestamp=...).
|
|
746
|
+
"""
|
|
747
|
+
self.own_sp = []
|
|
748
|
+
self.eff_sp = []
|
|
749
|
+
for ts, own, din, dout in zip(
|
|
750
|
+
self.timestamps, self.own_vests, self.delegated_vests_in, self.delegated_vests_out
|
|
751
|
+
):
|
|
752
|
+
sum_in = sum([din[key].amount for key in din])
|
|
753
|
+
sum_out = sum([dout[key].amount for key in dout])
|
|
754
|
+
sp_in = self.blockchain.vests_to_hp(sum_in, timestamp=ts)
|
|
755
|
+
sp_out = self.blockchain.vests_to_hp(sum_out, timestamp=ts)
|
|
756
|
+
sp_own = self.blockchain.vests_to_hp(own, timestamp=ts)
|
|
757
|
+
|
|
758
|
+
sp_eff = sp_own + sp_in - sp_out
|
|
759
|
+
self.own_sp.append(sp_own)
|
|
760
|
+
self.eff_sp.append(sp_eff)
|
|
761
|
+
|
|
762
|
+
def build_rep_arrays(self) -> None:
|
|
763
|
+
"""Build reputation arrays"""
|
|
764
|
+
self.rep_timestamp = [self.timestamps[1]]
|
|
765
|
+
self.rep = [reputation_to_score(0)]
|
|
766
|
+
current_reputation = 0
|
|
767
|
+
for ts, rshares, rep in zip(self.in_vote_timestamp, self.in_vote_rshares, self.in_vote_rep):
|
|
768
|
+
if rep > 0:
|
|
769
|
+
if rshares > 0 or (rshares < 0 and rep > current_reputation):
|
|
770
|
+
current_reputation += rshares >> 6
|
|
771
|
+
self.rep.append(reputation_to_score(current_reputation))
|
|
772
|
+
self.rep_timestamp.append(ts)
|
|
773
|
+
|
|
774
|
+
def build_vp_arrays(self) -> None:
|
|
775
|
+
"""
|
|
776
|
+
Build timelines for upvote and downvote voting power.
|
|
777
|
+
|
|
778
|
+
Populates the following instance arrays with parallel timestamps and voting-power values:
|
|
779
|
+
- self.vp_timestamp, self.vp: upvoting power timeline
|
|
780
|
+
- self.downvote_vp_timestamp, self.downvote_vp: downvoting power timeline
|
|
781
|
+
|
|
782
|
+
The method iterates over recorded outgoing votes (self.out_vote_timestamp / self.out_vote_weight),
|
|
783
|
+
applies Hive vote-regeneration rules (using HIVE_VOTE_REGENERATION_SECONDS and HIVE_100_PERCENT),
|
|
784
|
+
accounts for the HF_21 downvote timing change, and models vote drains via the blockchain's
|
|
785
|
+
_vote/resulting calculation and the account's manabar recharge intervals (account.get_manabar_recharge_timedelta).
|
|
786
|
+
Values are stored as integer percentage units where HIVE_100_PERCENT (typically 10000) represents 100.00%.
|
|
787
|
+
|
|
788
|
+
Side effects:
|
|
789
|
+
- Modifies self.vp_timestamp, self.vp, self.downvote_vp_timestamp, and self.downvote_vp in place.
|
|
790
|
+
"""
|
|
791
|
+
self.vp_timestamp = [self.timestamps[1]]
|
|
792
|
+
self.vp = [HIVE_100_PERCENT]
|
|
793
|
+
HF_21 = datetime(2019, 8, 27, 15, tzinfo=timezone.utc)
|
|
794
|
+
# Ensure timestamps[1] is datetime for comparison
|
|
795
|
+
ts1 = self.timestamps[1]
|
|
796
|
+
if isinstance(ts1, time):
|
|
797
|
+
ts1 = datetime.combine(datetime.now().date(), ts1)
|
|
798
|
+
elif isinstance(ts1, date):
|
|
799
|
+
ts1 = datetime.combine(ts1, time.min)
|
|
800
|
+
if ts1 is not None and ts1 > HF_21:
|
|
801
|
+
self.downvote_vp_timestamp = [self.timestamps[1]]
|
|
802
|
+
else:
|
|
803
|
+
self.downvote_vp_timestamp = [HF_21]
|
|
804
|
+
self.downvote_vp = [HIVE_100_PERCENT]
|
|
805
|
+
|
|
806
|
+
for ts, weight in zip(self.out_vote_timestamp, self.out_vote_weight):
|
|
807
|
+
regenerated_vp = 0
|
|
808
|
+
if ts > HF_21 and weight < 0:
|
|
809
|
+
self.downvote_vp.append(self.downvote_vp[-1])
|
|
810
|
+
if self.downvote_vp[-1] < HIVE_100_PERCENT:
|
|
811
|
+
regenerated_vp = (
|
|
812
|
+
((ts - self.downvote_vp_timestamp[-1]).total_seconds())
|
|
813
|
+
* HIVE_100_PERCENT
|
|
814
|
+
/ HIVE_VOTE_REGENERATION_SECONDS
|
|
815
|
+
)
|
|
816
|
+
self.downvote_vp[-1] += int(regenerated_vp)
|
|
817
|
+
|
|
818
|
+
if self.downvote_vp[-1] > HIVE_100_PERCENT:
|
|
819
|
+
self.downvote_vp[-1] = HIVE_100_PERCENT
|
|
820
|
+
recharge_time = self.account.get_manabar_recharge_timedelta(
|
|
821
|
+
{"current_mana_pct": self.downvote_vp[-2] / 100}
|
|
822
|
+
)
|
|
823
|
+
# Add full downvote VP once fully charged
|
|
824
|
+
last_ts = self.downvote_vp_timestamp[-1]
|
|
825
|
+
if isinstance(last_ts, time):
|
|
826
|
+
last_ts = datetime.combine(datetime.now().date(), last_ts)
|
|
827
|
+
elif isinstance(last_ts, date):
|
|
828
|
+
last_ts = datetime.combine(last_ts, time.min)
|
|
829
|
+
if last_ts is not None:
|
|
830
|
+
self.downvote_vp_timestamp.append(last_ts + recharge_time)
|
|
831
|
+
self.downvote_vp.append(HIVE_100_PERCENT)
|
|
832
|
+
|
|
833
|
+
# Add charged downvote VP just before new Vote
|
|
834
|
+
self.downvote_vp_timestamp.append(ts - timedelta(seconds=1))
|
|
835
|
+
self.downvote_vp.append(
|
|
836
|
+
min([HIVE_100_PERCENT, self.downvote_vp[-1] + regenerated_vp])
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
self.downvote_vp[-1] -= (
|
|
840
|
+
self.blockchain._calc_resulting_vote(HIVE_100_PERCENT, weight) * 4
|
|
841
|
+
)
|
|
842
|
+
# Downvote mana pool is 1/4th of the upvote mana pool, so it gets drained 4 times as quick
|
|
843
|
+
if self.downvote_vp[-1] < 0:
|
|
844
|
+
# There's most likely a better solution to this that what I did here
|
|
845
|
+
self.vp.append(self.vp[-1])
|
|
846
|
+
|
|
847
|
+
if self.vp[-1] < HIVE_100_PERCENT:
|
|
848
|
+
regenerated_vp = (
|
|
849
|
+
((ts - self.vp_timestamp[-1]).total_seconds())
|
|
850
|
+
* HIVE_100_PERCENT
|
|
851
|
+
/ HIVE_VOTE_REGENERATION_SECONDS
|
|
852
|
+
)
|
|
853
|
+
self.vp[-1] += int(regenerated_vp)
|
|
854
|
+
|
|
855
|
+
if self.vp[-1] > HIVE_100_PERCENT:
|
|
856
|
+
self.vp[-1] = HIVE_100_PERCENT
|
|
857
|
+
recharge_time = self.account.get_manabar_recharge_timedelta(
|
|
858
|
+
{"current_mana_pct": self.vp[-2] / 100}
|
|
859
|
+
)
|
|
860
|
+
# Add full VP once fully charged
|
|
861
|
+
last_vp_ts = self.vp_timestamp[-1]
|
|
862
|
+
if isinstance(last_vp_ts, time):
|
|
863
|
+
last_vp_ts = datetime.combine(datetime.now().date(), last_vp_ts)
|
|
864
|
+
elif isinstance(last_vp_ts, date):
|
|
865
|
+
last_vp_ts = datetime.combine(last_vp_ts, time.min)
|
|
866
|
+
if last_vp_ts is not None:
|
|
867
|
+
self.vp_timestamp.append(last_vp_ts + recharge_time)
|
|
868
|
+
self.vp.append(HIVE_100_PERCENT)
|
|
869
|
+
if self.vp[-1] == HIVE_100_PERCENT and ts - self.vp_timestamp[-1] > timedelta(
|
|
870
|
+
seconds=1
|
|
871
|
+
):
|
|
872
|
+
# Add charged VP just before new Vote
|
|
873
|
+
self.vp_timestamp.append(ts - timedelta(seconds=1))
|
|
874
|
+
self.vp.append(min([HIVE_100_PERCENT, self.vp[-1] + regenerated_vp]))
|
|
875
|
+
self.vp[-1] += self.downvote_vp[-1] / 4
|
|
876
|
+
if self.vp[-1] < 0:
|
|
877
|
+
self.vp[-1] = 0
|
|
878
|
+
|
|
879
|
+
self.vp_timestamp.append(ts)
|
|
880
|
+
self.downvote_vp[-1] = 0
|
|
881
|
+
self.downvote_vp_timestamp.append(ts)
|
|
882
|
+
|
|
883
|
+
else:
|
|
884
|
+
self.vp.append(self.vp[-1])
|
|
885
|
+
|
|
886
|
+
if self.vp[-1] < HIVE_100_PERCENT:
|
|
887
|
+
regenerated_vp = (
|
|
888
|
+
((ts - self.vp_timestamp[-1]).total_seconds())
|
|
889
|
+
* HIVE_100_PERCENT
|
|
890
|
+
/ HIVE_VOTE_REGENERATION_SECONDS
|
|
891
|
+
)
|
|
892
|
+
self.vp[-1] += int(regenerated_vp)
|
|
893
|
+
|
|
894
|
+
if self.vp[-1] > HIVE_100_PERCENT:
|
|
895
|
+
self.vp[-1] = HIVE_100_PERCENT
|
|
896
|
+
recharge_time = self.account.get_manabar_recharge_timedelta(
|
|
897
|
+
{"current_mana_pct": self.vp[-2] / 100}
|
|
898
|
+
)
|
|
899
|
+
# Add full VP once fully charged
|
|
900
|
+
last_vp_ts = self.vp_timestamp[-1]
|
|
901
|
+
if isinstance(last_vp_ts, time):
|
|
902
|
+
last_vp_ts = datetime.combine(datetime.now().date(), last_vp_ts)
|
|
903
|
+
elif isinstance(last_vp_ts, date):
|
|
904
|
+
last_vp_ts = datetime.combine(last_vp_ts, time.min)
|
|
905
|
+
if last_vp_ts is not None:
|
|
906
|
+
self.vp_timestamp.append(last_vp_ts + recharge_time)
|
|
907
|
+
self.vp.append(HIVE_100_PERCENT)
|
|
908
|
+
if self.vp[-1] == HIVE_100_PERCENT and ts - self.vp_timestamp[-1] > timedelta(
|
|
909
|
+
seconds=1
|
|
910
|
+
):
|
|
911
|
+
# Add charged VP just before new Vote
|
|
912
|
+
self.vp_timestamp.append(ts - timedelta(seconds=1))
|
|
913
|
+
self.vp.append(min([HIVE_100_PERCENT, self.vp[-1] + regenerated_vp]))
|
|
914
|
+
self.vp[-1] -= self.blockchain._calc_resulting_vote(self.vp[-1], weight)
|
|
915
|
+
if self.vp[-1] < 0:
|
|
916
|
+
self.vp[-1] = 0
|
|
917
|
+
|
|
918
|
+
self.vp_timestamp.append(ts)
|
|
919
|
+
|
|
920
|
+
if self.account.get_voting_power() == 100:
|
|
921
|
+
self.vp.append(10000)
|
|
922
|
+
recharge_time = self.account.get_manabar_recharge_timedelta(
|
|
923
|
+
{"current_mana_pct": self.vp[-2] / 100}
|
|
924
|
+
)
|
|
925
|
+
last_vp_ts = self.vp_timestamp[-1]
|
|
926
|
+
if isinstance(last_vp_ts, time):
|
|
927
|
+
last_vp_ts = datetime.combine(datetime.now().date(), last_vp_ts)
|
|
928
|
+
elif isinstance(last_vp_ts, date):
|
|
929
|
+
last_vp_ts = datetime.combine(last_vp_ts, time.min)
|
|
930
|
+
if last_vp_ts is not None:
|
|
931
|
+
self.vp_timestamp.append(last_vp_ts + recharge_time)
|
|
932
|
+
|
|
933
|
+
if self.account.get_downvoting_power() == 100:
|
|
934
|
+
self.downvote_vp.append(10000)
|
|
935
|
+
recharge_time = self.account.get_manabar_recharge_timedelta(
|
|
936
|
+
{"current_mana_pct": self.downvote_vp[-2] / 100}
|
|
937
|
+
)
|
|
938
|
+
last_downvote_ts = self.downvote_vp_timestamp[-1]
|
|
939
|
+
if isinstance(last_downvote_ts, time):
|
|
940
|
+
last_downvote_ts = datetime.combine(datetime.now().date(), last_downvote_ts)
|
|
941
|
+
elif isinstance(last_downvote_ts, date):
|
|
942
|
+
last_downvote_ts = datetime.combine(last_downvote_ts, time.min)
|
|
943
|
+
if last_downvote_ts is not None:
|
|
944
|
+
self.downvote_vp_timestamp.append(last_downvote_ts + recharge_time)
|
|
945
|
+
|
|
946
|
+
self.vp.append(self.account.get_voting_power() * 100)
|
|
947
|
+
self.downvote_vp.append(self.account.get_downvoting_power() * 100)
|
|
948
|
+
self.downvote_vp_timestamp.append(datetime.now(timezone.utc))
|
|
949
|
+
self.vp_timestamp.append(datetime.now(timezone.utc))
|
|
950
|
+
|
|
951
|
+
def build_curation_arrays(
|
|
952
|
+
self, end_date: Optional[Union[datetime, date, time]] = None, sum_days: int = 7
|
|
953
|
+
) -> None:
|
|
954
|
+
"""
|
|
955
|
+
Compute curation-per-1000-HP time series and store them in
|
|
956
|
+
self.curation_per_1000_HP_timestamp and self.curation_per_1000_HP.
|
|
957
|
+
|
|
958
|
+
The method walks through recorded reward timestamps and curation rewards, converts
|
|
959
|
+
each curation reward (vests) to HP using the blockchain conversion, and divides
|
|
960
|
+
that reward by the effective stake (sp_eff) at the reward time to produce a
|
|
961
|
+
"curation per 1000 HP" value. Values are aggregated into contiguous windows of
|
|
962
|
+
length `sum_days`. Each window's aggregate is appended to
|
|
963
|
+
self.curation_per_1000_HP with the corresponding window end timestamp in
|
|
964
|
+
self.curation_per_1000_HP_timestamp.
|
|
965
|
+
|
|
966
|
+
Parameters:
|
|
967
|
+
end_date (datetime.datetime | None): End-boundary for the first aggregation
|
|
968
|
+
window. If None, it is set to the last reward timestamp minus the total
|
|
969
|
+
span of full `sum_days` windows that fit into the reward history.
|
|
970
|
+
sum_days (int): Window length in days for aggregation. Must be > 0.
|
|
971
|
+
|
|
972
|
+
Raises:
|
|
973
|
+
ValueError: If sum_days <= 0.
|
|
974
|
+
|
|
975
|
+
Notes:
|
|
976
|
+
- Uses self.blockchain.vests_to_hp(vests, timestamp=ts) to convert vests to HP.
|
|
977
|
+
- Uses self.get_data(timestamp=ts, index=index) to obtain the effective stake
|
|
978
|
+
(`sp_eff`) and to advance a cached index for efficient lookups.
|
|
979
|
+
- The per-window aggregation normalizes values to a "per 1000 HP" basis and
|
|
980
|
+
scales them by (7 / sum_days) so the resulting numbers are comparable to a
|
|
981
|
+
7-day baseline.
|
|
982
|
+
"""
|
|
983
|
+
self.curation_per_1000_HP_timestamp = []
|
|
984
|
+
self.curation_per_1000_HP = []
|
|
985
|
+
if sum_days <= 0:
|
|
986
|
+
raise ValueError("sum_days must be greater than 0")
|
|
987
|
+
index = 0
|
|
988
|
+
curation_sum = 0
|
|
989
|
+
days = (self.reward_timestamps[-1] - self.reward_timestamps[0]).days // sum_days * sum_days
|
|
990
|
+
if end_date is None:
|
|
991
|
+
end_date = self.reward_timestamps[-1] - timedelta(days=days)
|
|
992
|
+
for ts, vests in zip(self.reward_timestamps, self.curation_rewards):
|
|
993
|
+
if vests == 0:
|
|
994
|
+
continue
|
|
995
|
+
sp = self.blockchain.vests_to_hp(vests, timestamp=ts)
|
|
996
|
+
data = self.get_data(timestamp=ts, index=index)
|
|
997
|
+
index = data["index"]
|
|
998
|
+
if "sp_eff" in data and data["sp_eff"] > 0:
|
|
999
|
+
curation_1k_sp = sp / data["sp_eff"] * 1000 / sum_days * 7
|
|
1000
|
+
else:
|
|
1001
|
+
curation_1k_sp = 0
|
|
1002
|
+
if ts < end_date:
|
|
1003
|
+
curation_sum += curation_1k_sp
|
|
1004
|
+
else:
|
|
1005
|
+
self.curation_per_1000_HP_timestamp.append(end_date)
|
|
1006
|
+
self.curation_per_1000_HP.append(curation_sum)
|
|
1007
|
+
# Ensure end_date is a datetime for arithmetic
|
|
1008
|
+
if isinstance(end_date, datetime):
|
|
1009
|
+
end_date = end_date + timedelta(days=sum_days)
|
|
1010
|
+
elif isinstance(end_date, date):
|
|
1011
|
+
end_date = datetime.combine(end_date, time.min, timezone.utc) + timedelta(
|
|
1012
|
+
days=sum_days
|
|
1013
|
+
)
|
|
1014
|
+
else: # time object
|
|
1015
|
+
end_date = datetime.combine(date.today(), end_date, timezone.utc) + timedelta(
|
|
1016
|
+
days=sum_days
|
|
1017
|
+
)
|
|
1018
|
+
curation_sum = 0
|
|
1019
|
+
|
|
1020
|
+
def __str__(self) -> str:
|
|
1021
|
+
return self.__repr__()
|
|
1022
|
+
|
|
1023
|
+
def __repr__(self) -> str:
|
|
1024
|
+
return "<{} {}>".format(self.__class__.__name__, str(self.account["name"]))
|