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.
Files changed (87) hide show
  1. hive_nectar-0.2.9.dist-info/METADATA +194 -0
  2. hive_nectar-0.2.9.dist-info/RECORD +87 -0
  3. hive_nectar-0.2.9.dist-info/WHEEL +4 -0
  4. hive_nectar-0.2.9.dist-info/entry_points.txt +2 -0
  5. hive_nectar-0.2.9.dist-info/licenses/LICENSE.txt +23 -0
  6. nectar/__init__.py +37 -0
  7. nectar/account.py +5076 -0
  8. nectar/amount.py +553 -0
  9. nectar/asciichart.py +303 -0
  10. nectar/asset.py +122 -0
  11. nectar/block.py +574 -0
  12. nectar/blockchain.py +1242 -0
  13. nectar/blockchaininstance.py +2590 -0
  14. nectar/blockchainobject.py +263 -0
  15. nectar/cli.py +5937 -0
  16. nectar/comment.py +1552 -0
  17. nectar/community.py +854 -0
  18. nectar/constants.py +95 -0
  19. nectar/discussions.py +1437 -0
  20. nectar/exceptions.py +152 -0
  21. nectar/haf.py +381 -0
  22. nectar/hive.py +630 -0
  23. nectar/imageuploader.py +114 -0
  24. nectar/instance.py +113 -0
  25. nectar/market.py +876 -0
  26. nectar/memo.py +542 -0
  27. nectar/message.py +379 -0
  28. nectar/nodelist.py +309 -0
  29. nectar/price.py +603 -0
  30. nectar/profile.py +74 -0
  31. nectar/py.typed +0 -0
  32. nectar/rc.py +333 -0
  33. nectar/snapshot.py +1024 -0
  34. nectar/storage.py +62 -0
  35. nectar/transactionbuilder.py +659 -0
  36. nectar/utils.py +630 -0
  37. nectar/version.py +3 -0
  38. nectar/vote.py +722 -0
  39. nectar/wallet.py +472 -0
  40. nectar/witness.py +728 -0
  41. nectarapi/__init__.py +12 -0
  42. nectarapi/exceptions.py +126 -0
  43. nectarapi/graphenerpc.py +596 -0
  44. nectarapi/node.py +194 -0
  45. nectarapi/noderpc.py +79 -0
  46. nectarapi/openapi.py +107 -0
  47. nectarapi/py.typed +0 -0
  48. nectarapi/rpcutils.py +98 -0
  49. nectarapi/version.py +3 -0
  50. nectarbase/__init__.py +15 -0
  51. nectarbase/ledgertransactions.py +106 -0
  52. nectarbase/memo.py +242 -0
  53. nectarbase/objects.py +521 -0
  54. nectarbase/objecttypes.py +21 -0
  55. nectarbase/operationids.py +102 -0
  56. nectarbase/operations.py +1357 -0
  57. nectarbase/py.typed +0 -0
  58. nectarbase/signedtransactions.py +89 -0
  59. nectarbase/transactions.py +11 -0
  60. nectarbase/version.py +3 -0
  61. nectargraphenebase/__init__.py +27 -0
  62. nectargraphenebase/account.py +1121 -0
  63. nectargraphenebase/aes.py +49 -0
  64. nectargraphenebase/base58.py +197 -0
  65. nectargraphenebase/bip32.py +575 -0
  66. nectargraphenebase/bip38.py +110 -0
  67. nectargraphenebase/chains.py +15 -0
  68. nectargraphenebase/dictionary.py +2 -0
  69. nectargraphenebase/ecdsasig.py +309 -0
  70. nectargraphenebase/objects.py +130 -0
  71. nectargraphenebase/objecttypes.py +8 -0
  72. nectargraphenebase/operationids.py +5 -0
  73. nectargraphenebase/operations.py +25 -0
  74. nectargraphenebase/prefix.py +13 -0
  75. nectargraphenebase/py.typed +0 -0
  76. nectargraphenebase/signedtransactions.py +221 -0
  77. nectargraphenebase/types.py +557 -0
  78. nectargraphenebase/unsignedtransactions.py +288 -0
  79. nectargraphenebase/version.py +3 -0
  80. nectarstorage/__init__.py +57 -0
  81. nectarstorage/base.py +317 -0
  82. nectarstorage/exceptions.py +15 -0
  83. nectarstorage/interfaces.py +244 -0
  84. nectarstorage/masterpassword.py +237 -0
  85. nectarstorage/py.typed +0 -0
  86. nectarstorage/ram.py +27 -0
  87. 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"]))