hive-nectar 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hive-nectar might be problematic. Click here for more details.
- hive_nectar-0.0.2.dist-info/METADATA +182 -0
- hive_nectar-0.0.2.dist-info/RECORD +86 -0
- hive_nectar-0.0.2.dist-info/WHEEL +4 -0
- hive_nectar-0.0.2.dist-info/entry_points.txt +2 -0
- hive_nectar-0.0.2.dist-info/licenses/LICENSE.txt +23 -0
- nectar/__init__.py +32 -0
- nectar/account.py +4371 -0
- nectar/amount.py +475 -0
- nectar/asciichart.py +270 -0
- nectar/asset.py +82 -0
- nectar/block.py +446 -0
- nectar/blockchain.py +1178 -0
- nectar/blockchaininstance.py +2284 -0
- nectar/blockchainobject.py +221 -0
- nectar/blurt.py +563 -0
- nectar/cli.py +6285 -0
- nectar/comment.py +1217 -0
- nectar/community.py +513 -0
- nectar/constants.py +111 -0
- nectar/conveyor.py +309 -0
- nectar/discussions.py +1709 -0
- nectar/exceptions.py +149 -0
- nectar/hive.py +546 -0
- nectar/hivesigner.py +420 -0
- nectar/imageuploader.py +72 -0
- nectar/instance.py +129 -0
- nectar/market.py +1013 -0
- nectar/memo.py +449 -0
- nectar/message.py +357 -0
- nectar/nodelist.py +444 -0
- nectar/price.py +557 -0
- nectar/profile.py +65 -0
- nectar/rc.py +308 -0
- nectar/snapshot.py +726 -0
- nectar/steem.py +582 -0
- nectar/storage.py +53 -0
- nectar/transactionbuilder.py +622 -0
- nectar/utils.py +545 -0
- nectar/version.py +2 -0
- nectar/vote.py +557 -0
- nectar/wallet.py +472 -0
- nectar/witness.py +617 -0
- nectarapi/__init__.py +11 -0
- nectarapi/exceptions.py +123 -0
- nectarapi/graphenerpc.py +589 -0
- nectarapi/node.py +178 -0
- nectarapi/noderpc.py +229 -0
- nectarapi/rpcutils.py +97 -0
- nectarapi/version.py +2 -0
- nectarbase/__init__.py +14 -0
- nectarbase/ledgertransactions.py +75 -0
- nectarbase/memo.py +243 -0
- nectarbase/objects.py +429 -0
- nectarbase/objecttypes.py +22 -0
- nectarbase/operationids.py +102 -0
- nectarbase/operations.py +1297 -0
- nectarbase/signedtransactions.py +48 -0
- nectarbase/transactions.py +11 -0
- nectarbase/version.py +2 -0
- nectargrapheneapi/__init__.py +6 -0
- nectargraphenebase/__init__.py +27 -0
- nectargraphenebase/account.py +846 -0
- nectargraphenebase/aes.py +52 -0
- nectargraphenebase/base58.py +192 -0
- nectargraphenebase/bip32.py +494 -0
- nectargraphenebase/bip38.py +134 -0
- nectargraphenebase/chains.py +149 -0
- nectargraphenebase/dictionary.py +3 -0
- nectargraphenebase/ecdsasig.py +326 -0
- nectargraphenebase/objects.py +123 -0
- nectargraphenebase/objecttypes.py +6 -0
- nectargraphenebase/operationids.py +3 -0
- nectargraphenebase/operations.py +23 -0
- nectargraphenebase/prefix.py +11 -0
- nectargraphenebase/py23.py +38 -0
- nectargraphenebase/signedtransactions.py +201 -0
- nectargraphenebase/types.py +419 -0
- nectargraphenebase/unsignedtransactions.py +283 -0
- nectargraphenebase/version.py +2 -0
- nectarstorage/__init__.py +38 -0
- nectarstorage/base.py +306 -0
- nectarstorage/exceptions.py +16 -0
- nectarstorage/interfaces.py +237 -0
- nectarstorage/masterpassword.py +239 -0
- nectarstorage/ram.py +30 -0
- nectarstorage/sqlite.py +334 -0
nectar/account.py
ADDED
|
@@ -0,0 +1,4371 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import random
|
|
5
|
+
from datetime import date, datetime, time, timedelta, timezone
|
|
6
|
+
|
|
7
|
+
from prettytable import PrettyTable
|
|
8
|
+
|
|
9
|
+
from nectar.amount import Amount
|
|
10
|
+
from nectar.constants import (
|
|
11
|
+
STEEM_1_PERCENT,
|
|
12
|
+
STEEM_100_PERCENT,
|
|
13
|
+
STEEM_VOTE_REGENERATION_SECONDS,
|
|
14
|
+
STEEM_VOTING_MANA_REGENERATION_SECONDS,
|
|
15
|
+
)
|
|
16
|
+
from nectar.instance import shared_blockchain_instance
|
|
17
|
+
from nectar.rc import RC
|
|
18
|
+
from nectarapi.exceptions import (
|
|
19
|
+
ApiNotSupported,
|
|
20
|
+
FilteredItemNotFound,
|
|
21
|
+
MissingRequiredActiveAuthority,
|
|
22
|
+
SupportedByHivemind,
|
|
23
|
+
)
|
|
24
|
+
from nectarbase import operations
|
|
25
|
+
from nectargraphenebase.account import PasswordKey, PublicKey
|
|
26
|
+
from nectargraphenebase.py23 import integer_types, string_types
|
|
27
|
+
|
|
28
|
+
from .blockchain import Blockchain
|
|
29
|
+
from .blockchainobject import BlockchainObject
|
|
30
|
+
from .exceptions import AccountDoesNotExistsException, OfflineHasNoRPCException
|
|
31
|
+
from .utils import (
|
|
32
|
+
addTzInfo,
|
|
33
|
+
formatTimedelta,
|
|
34
|
+
formatTimeString,
|
|
35
|
+
remove_from_dict,
|
|
36
|
+
reputation_to_score,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
log = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def extract_account_name(account):
|
|
43
|
+
if isinstance(account, str):
|
|
44
|
+
return account
|
|
45
|
+
elif isinstance(account, Account):
|
|
46
|
+
return account["name"]
|
|
47
|
+
elif isinstance(account, dict) and "name" in account:
|
|
48
|
+
return account["name"]
|
|
49
|
+
else:
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Account(BlockchainObject):
|
|
54
|
+
"""This class allows to easily access Account data
|
|
55
|
+
|
|
56
|
+
:param str account: Name of the account
|
|
57
|
+
:param Steem/Hive blockchain_instance: Hive or Steem
|
|
58
|
+
instance
|
|
59
|
+
:param bool lazy: Use lazy loading
|
|
60
|
+
:param bool full: Obtain all account data including orders, positions,
|
|
61
|
+
etc.
|
|
62
|
+
:param Hive hive_instance: Hive instance
|
|
63
|
+
:param Steem steem_instance: Steem instance
|
|
64
|
+
:returns: Account data
|
|
65
|
+
:rtype: dictionary
|
|
66
|
+
:raises nectar.exceptions.AccountDoesNotExistsException: if account
|
|
67
|
+
does not exist
|
|
68
|
+
|
|
69
|
+
Instances of this class are dictionaries that come with additional
|
|
70
|
+
methods (see below) that allow dealing with an account and its
|
|
71
|
+
corresponding functions.
|
|
72
|
+
|
|
73
|
+
.. code-block:: python
|
|
74
|
+
|
|
75
|
+
>>> from nectar.account import Account
|
|
76
|
+
>>> from nectar import Hive
|
|
77
|
+
>>> from nectar.nodelist import NodeList
|
|
78
|
+
>>> nodelist = NodeList()
|
|
79
|
+
>>> nodelist.update_nodes()
|
|
80
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
81
|
+
>>> account = Account("gtg", blockchain_instance=stm)
|
|
82
|
+
>>> print(account)
|
|
83
|
+
<Account gtg>
|
|
84
|
+
>>> print(account.balances) # doctest: +SKIP
|
|
85
|
+
|
|
86
|
+
.. note:: This class comes with its own caching function to reduce the
|
|
87
|
+
load on the API server. Instances of this class can be
|
|
88
|
+
refreshed with ``Account.refresh()``. The cache can be
|
|
89
|
+
cleared with ``Account.clear_cache()``
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
type_id = 2
|
|
94
|
+
|
|
95
|
+
def __init__(self, account, full=True, lazy=False, blockchain_instance=None, **kwargs):
|
|
96
|
+
"""Initialize an account
|
|
97
|
+
|
|
98
|
+
:param str account: Name of the account
|
|
99
|
+
:param Steem blockchain_instance: Steem
|
|
100
|
+
instance
|
|
101
|
+
:param bool lazy: Use lazy loading
|
|
102
|
+
:param bool full: Obtain all account data including orders, positions,
|
|
103
|
+
etc.
|
|
104
|
+
"""
|
|
105
|
+
self.full = full
|
|
106
|
+
self.lazy = lazy
|
|
107
|
+
if blockchain_instance is None:
|
|
108
|
+
if kwargs.get("steem_instance"):
|
|
109
|
+
blockchain_instance = kwargs["steem_instance"]
|
|
110
|
+
elif kwargs.get("hive_instance"):
|
|
111
|
+
blockchain_instance = kwargs["hive_instance"]
|
|
112
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
113
|
+
if isinstance(account, dict):
|
|
114
|
+
account = self._parse_json_data(account)
|
|
115
|
+
super(Account, self).__init__(
|
|
116
|
+
account, lazy=lazy, full=full, id_item="name", blockchain_instance=blockchain_instance
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def refresh(self):
|
|
120
|
+
"""Refresh/Obtain an account's data from the API server"""
|
|
121
|
+
if not self.blockchain.is_connected():
|
|
122
|
+
return
|
|
123
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(self.blockchain.rpc.get_use_appbase())
|
|
124
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
125
|
+
account = self.blockchain.rpc.find_accounts(
|
|
126
|
+
{"accounts": [self.identifier]}, api="database"
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
if self.full:
|
|
130
|
+
account = self.blockchain.rpc.get_accounts([self.identifier], api="database")
|
|
131
|
+
else:
|
|
132
|
+
account = self.blockchain.rpc.lookup_account_names(
|
|
133
|
+
[self.identifier], api="database"
|
|
134
|
+
)
|
|
135
|
+
if self.blockchain.rpc.get_use_appbase() and "accounts" in account:
|
|
136
|
+
account = account["accounts"]
|
|
137
|
+
if account and isinstance(account, list) and len(account) == 1:
|
|
138
|
+
account = account[0]
|
|
139
|
+
if not account:
|
|
140
|
+
raise AccountDoesNotExistsException(self.identifier)
|
|
141
|
+
account = self._parse_json_data(account)
|
|
142
|
+
self.identifier = account["name"]
|
|
143
|
+
# self.blockchain.refresh_data()
|
|
144
|
+
|
|
145
|
+
super(Account, self).__init__(
|
|
146
|
+
account,
|
|
147
|
+
id_item="name",
|
|
148
|
+
lazy=self.lazy,
|
|
149
|
+
full=self.full,
|
|
150
|
+
blockchain_instance=self.blockchain,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _parse_json_data(self, account):
|
|
154
|
+
parse_int = [
|
|
155
|
+
"sbd_seconds",
|
|
156
|
+
"savings_sbd_seconds",
|
|
157
|
+
"average_bandwidth",
|
|
158
|
+
"lifetime_bandwidth",
|
|
159
|
+
"lifetime_market_bandwidth",
|
|
160
|
+
"reputation",
|
|
161
|
+
"withdrawn",
|
|
162
|
+
"to_withdraw",
|
|
163
|
+
"hbd_seconds",
|
|
164
|
+
"savings_hbd_seconds",
|
|
165
|
+
]
|
|
166
|
+
for p in parse_int:
|
|
167
|
+
if p in account and isinstance(account.get(p), string_types):
|
|
168
|
+
account[p] = int(account.get(p, 0))
|
|
169
|
+
if "proxied_vsf_votes" in account:
|
|
170
|
+
proxied_vsf_votes = []
|
|
171
|
+
for p_int in account["proxied_vsf_votes"]:
|
|
172
|
+
if isinstance(p_int, string_types):
|
|
173
|
+
proxied_vsf_votes.append(int(p_int))
|
|
174
|
+
else:
|
|
175
|
+
proxied_vsf_votes.append(p_int)
|
|
176
|
+
account["proxied_vsf_votes"] = proxied_vsf_votes
|
|
177
|
+
parse_times = [
|
|
178
|
+
"last_owner_update",
|
|
179
|
+
"last_account_update",
|
|
180
|
+
"created",
|
|
181
|
+
"last_owner_proved",
|
|
182
|
+
"last_active_proved",
|
|
183
|
+
"last_account_recovery",
|
|
184
|
+
"last_vote_time",
|
|
185
|
+
"sbd_seconds_last_update",
|
|
186
|
+
"sbd_last_interest_payment",
|
|
187
|
+
"savings_sbd_seconds_last_update",
|
|
188
|
+
"savings_sbd_last_interest_payment",
|
|
189
|
+
"next_vesting_withdrawal",
|
|
190
|
+
"last_market_bandwidth_update",
|
|
191
|
+
"last_post",
|
|
192
|
+
"last_root_post",
|
|
193
|
+
"last_bandwidth_update",
|
|
194
|
+
"hbd_seconds_last_update",
|
|
195
|
+
"hbd_last_interest_payment",
|
|
196
|
+
"savings_hbd_seconds_last_update",
|
|
197
|
+
"savings_hbd_last_interest_payment",
|
|
198
|
+
]
|
|
199
|
+
for p in parse_times:
|
|
200
|
+
if p in account and isinstance(account.get(p), string_types):
|
|
201
|
+
account[p] = formatTimeString(account.get(p, "1970-01-01T00:00:00"))
|
|
202
|
+
# Parse Amounts
|
|
203
|
+
amounts = [
|
|
204
|
+
"balance",
|
|
205
|
+
"savings_balance",
|
|
206
|
+
"sbd_balance",
|
|
207
|
+
"savings_sbd_balance",
|
|
208
|
+
"reward_sbd_balance",
|
|
209
|
+
"hbd_balance",
|
|
210
|
+
"savings_hbd_balance",
|
|
211
|
+
"reward_hbd_balance",
|
|
212
|
+
"reward_steem_balance",
|
|
213
|
+
"reward_hive_balance",
|
|
214
|
+
"reward_vesting_balance",
|
|
215
|
+
"reward_vesting_steem",
|
|
216
|
+
"vesting_shares",
|
|
217
|
+
"delegated_vesting_shares",
|
|
218
|
+
"received_vesting_shares",
|
|
219
|
+
"vesting_withdraw_rate",
|
|
220
|
+
"vesting_balance",
|
|
221
|
+
]
|
|
222
|
+
for p in amounts:
|
|
223
|
+
if p in account and isinstance(account.get(p), (string_types, list, dict)):
|
|
224
|
+
account[p] = Amount(account[p], blockchain_instance=self.blockchain)
|
|
225
|
+
return account
|
|
226
|
+
|
|
227
|
+
def json(self):
|
|
228
|
+
output = self.copy()
|
|
229
|
+
parse_int = [
|
|
230
|
+
"sbd_seconds",
|
|
231
|
+
"savings_sbd_seconds",
|
|
232
|
+
"hbd_seconds",
|
|
233
|
+
"savings_hbd_seconds",
|
|
234
|
+
]
|
|
235
|
+
parse_int_without_zero = [
|
|
236
|
+
"withdrawn",
|
|
237
|
+
"to_withdraw",
|
|
238
|
+
"lifetime_bandwidth",
|
|
239
|
+
"average_bandwidth",
|
|
240
|
+
]
|
|
241
|
+
for p in parse_int:
|
|
242
|
+
if p in output and isinstance(output[p], integer_types):
|
|
243
|
+
output[p] = str(output[p])
|
|
244
|
+
for p in parse_int_without_zero:
|
|
245
|
+
if p in output and isinstance(output[p], integer_types) and output[p] != 0:
|
|
246
|
+
output[p] = str(output[p])
|
|
247
|
+
if "proxied_vsf_votes" in output:
|
|
248
|
+
proxied_vsf_votes = []
|
|
249
|
+
for p_int in output["proxied_vsf_votes"]:
|
|
250
|
+
if isinstance(p_int, integer_types) and p_int != 0:
|
|
251
|
+
proxied_vsf_votes.append(str(p_int))
|
|
252
|
+
else:
|
|
253
|
+
proxied_vsf_votes.append(p_int)
|
|
254
|
+
output["proxied_vsf_votes"] = proxied_vsf_votes
|
|
255
|
+
parse_times = [
|
|
256
|
+
"last_owner_update",
|
|
257
|
+
"last_account_update",
|
|
258
|
+
"created",
|
|
259
|
+
"last_owner_proved",
|
|
260
|
+
"last_active_proved",
|
|
261
|
+
"last_account_recovery",
|
|
262
|
+
"last_vote_time",
|
|
263
|
+
"sbd_seconds_last_update",
|
|
264
|
+
"sbd_last_interest_payment",
|
|
265
|
+
"savings_sbd_seconds_last_update",
|
|
266
|
+
"savings_sbd_last_interest_payment",
|
|
267
|
+
"next_vesting_withdrawal",
|
|
268
|
+
"last_market_bandwidth_update",
|
|
269
|
+
"last_post",
|
|
270
|
+
"last_root_post",
|
|
271
|
+
"last_bandwidth_update",
|
|
272
|
+
"hbd_seconds_last_update",
|
|
273
|
+
"hbd_last_interest_payment",
|
|
274
|
+
"savings_hbd_seconds_last_update",
|
|
275
|
+
"savings_hbd_last_interest_payment",
|
|
276
|
+
]
|
|
277
|
+
for p in parse_times:
|
|
278
|
+
if p in output:
|
|
279
|
+
p_date = output.get(p, datetime(1970, 1, 1, 0, 0))
|
|
280
|
+
if isinstance(p_date, (datetime, date, time)):
|
|
281
|
+
output[p] = formatTimeString(p_date)
|
|
282
|
+
else:
|
|
283
|
+
output[p] = p_date
|
|
284
|
+
amounts = [
|
|
285
|
+
"balance",
|
|
286
|
+
"savings_balance",
|
|
287
|
+
"sbd_balance",
|
|
288
|
+
"savings_sbd_balance",
|
|
289
|
+
"reward_sbd_balance",
|
|
290
|
+
"reward_steem_balance",
|
|
291
|
+
"hbd_balance",
|
|
292
|
+
"savings_hbd_balance",
|
|
293
|
+
"reward_hbd_balance",
|
|
294
|
+
"reward_hive_balance",
|
|
295
|
+
"reward_vesting_balance",
|
|
296
|
+
"reward_vesting_steem",
|
|
297
|
+
"vesting_shares",
|
|
298
|
+
"delegated_vesting_shares",
|
|
299
|
+
"received_vesting_shares",
|
|
300
|
+
"vesting_withdraw_rate",
|
|
301
|
+
"vesting_balance",
|
|
302
|
+
]
|
|
303
|
+
for p in amounts:
|
|
304
|
+
if p in output:
|
|
305
|
+
if p in output:
|
|
306
|
+
output[p] = output.get(p).json()
|
|
307
|
+
return json.loads(str(json.dumps(output)))
|
|
308
|
+
|
|
309
|
+
def getSimilarAccountNames(self, limit=5):
|
|
310
|
+
"""Deprecated, please use get_similar_account_names"""
|
|
311
|
+
return self.get_similar_account_names(limit=limit)
|
|
312
|
+
|
|
313
|
+
def get_rc(self):
|
|
314
|
+
"""Return RC of account"""
|
|
315
|
+
b = Blockchain(blockchain_instance=self.blockchain)
|
|
316
|
+
return b.find_rc_accounts(self["name"])
|
|
317
|
+
|
|
318
|
+
def get_rc_manabar(self):
|
|
319
|
+
"""Returns current_mana and max_mana for RC"""
|
|
320
|
+
rc_param = self.get_rc()
|
|
321
|
+
max_mana = int(rc_param["max_rc"])
|
|
322
|
+
last_mana = int(rc_param["rc_manabar"]["current_mana"])
|
|
323
|
+
last_update_time = rc_param["rc_manabar"]["last_update_time"]
|
|
324
|
+
last_update = datetime.fromtimestamp(last_update_time, tz=timezone.utc)
|
|
325
|
+
diff_in_seconds = (datetime.now(timezone.utc) - last_update).total_seconds()
|
|
326
|
+
current_mana = int(
|
|
327
|
+
last_mana + diff_in_seconds * max_mana / STEEM_VOTING_MANA_REGENERATION_SECONDS
|
|
328
|
+
)
|
|
329
|
+
if current_mana > max_mana:
|
|
330
|
+
current_mana = max_mana
|
|
331
|
+
if max_mana > 0:
|
|
332
|
+
current_pct = current_mana / max_mana * 100
|
|
333
|
+
else:
|
|
334
|
+
current_pct = 0
|
|
335
|
+
max_rc_creation_adjustment = Amount(
|
|
336
|
+
rc_param["max_rc_creation_adjustment"], blockchain_instance=self.blockchain
|
|
337
|
+
)
|
|
338
|
+
return {
|
|
339
|
+
"last_mana": last_mana,
|
|
340
|
+
"last_update_time": last_update_time,
|
|
341
|
+
"current_mana": current_mana,
|
|
342
|
+
"max_mana": max_mana,
|
|
343
|
+
"current_pct": current_pct,
|
|
344
|
+
"max_rc_creation_adjustment": max_rc_creation_adjustment,
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
def get_similar_account_names(self, limit=5):
|
|
348
|
+
"""Returns ``limit`` account names similar to the current account
|
|
349
|
+
name as a list
|
|
350
|
+
|
|
351
|
+
:param int limit: limits the number of accounts, which will be
|
|
352
|
+
returned
|
|
353
|
+
:returns: Similar account names as list
|
|
354
|
+
:rtype: list
|
|
355
|
+
|
|
356
|
+
This is a wrapper around :func:`nectar.blockchain.Blockchain.get_similar_account_names()`
|
|
357
|
+
using the current account name as reference.
|
|
358
|
+
|
|
359
|
+
"""
|
|
360
|
+
b = Blockchain(blockchain_instance=self.blockchain)
|
|
361
|
+
return b.get_similar_account_names(self.name, limit=limit)
|
|
362
|
+
|
|
363
|
+
@property
|
|
364
|
+
def name(self):
|
|
365
|
+
"""Returns the account name"""
|
|
366
|
+
return self["name"]
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def profile(self):
|
|
370
|
+
"""Returns the account profile"""
|
|
371
|
+
metadata = self.json_metadata
|
|
372
|
+
if "profile" in metadata:
|
|
373
|
+
return metadata["profile"]
|
|
374
|
+
else:
|
|
375
|
+
return {}
|
|
376
|
+
|
|
377
|
+
@property
|
|
378
|
+
def rep(self):
|
|
379
|
+
"""Returns the account reputation"""
|
|
380
|
+
return self.get_reputation()
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def sp(self):
|
|
384
|
+
"""Returns the accounts Steem Power"""
|
|
385
|
+
return self.get_token_power()
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def tp(self):
|
|
389
|
+
"""Returns the accounts Hive/Steem Power"""
|
|
390
|
+
return self.get_token_power()
|
|
391
|
+
|
|
392
|
+
@property
|
|
393
|
+
def vp(self):
|
|
394
|
+
"""Returns the account voting power in the range of 0-100%"""
|
|
395
|
+
return self.get_voting_power()
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def json_metadata(self):
|
|
399
|
+
if self["json_metadata"] == "":
|
|
400
|
+
return {}
|
|
401
|
+
return json.loads(self["json_metadata"])
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def posting_json_metadata(self):
|
|
405
|
+
if self["posting_json_metadata"] == "":
|
|
406
|
+
return {}
|
|
407
|
+
return json.loads(self["posting_json_metadata"])
|
|
408
|
+
|
|
409
|
+
def print_info(self, force_refresh=False, return_str=False, use_table=False, **kwargs):
|
|
410
|
+
"""Prints import information about the account"""
|
|
411
|
+
if force_refresh:
|
|
412
|
+
self.refresh()
|
|
413
|
+
self.blockchain.refresh_data(True)
|
|
414
|
+
bandwidth = self.get_bandwidth()
|
|
415
|
+
if (
|
|
416
|
+
bandwidth is not None
|
|
417
|
+
and bandwidth["allocated"] is not None
|
|
418
|
+
and bandwidth["allocated"] > 0
|
|
419
|
+
):
|
|
420
|
+
remaining = 100 - bandwidth["used"] / bandwidth["allocated"] * 100
|
|
421
|
+
used_kb = bandwidth["used"] / 1024
|
|
422
|
+
allocated_mb = bandwidth["allocated"] / 1024 / 1024
|
|
423
|
+
last_vote_time_str = formatTimedelta(
|
|
424
|
+
addTzInfo(datetime.now(timezone.utc)) - self["last_vote_time"]
|
|
425
|
+
)
|
|
426
|
+
try:
|
|
427
|
+
rc_mana = self.get_rc_manabar()
|
|
428
|
+
rc = self.get_rc()
|
|
429
|
+
rc_calc = RC(blockchain_instance=self.blockchain)
|
|
430
|
+
except:
|
|
431
|
+
rc_mana = None
|
|
432
|
+
rc_calc = None
|
|
433
|
+
|
|
434
|
+
if use_table:
|
|
435
|
+
t = PrettyTable(["Key", "Value"])
|
|
436
|
+
t.align = "l"
|
|
437
|
+
t.add_row(["Name (rep)", self.name + " (%.2f)" % (self.rep)])
|
|
438
|
+
t.add_row(["Voting Power", "%.2f %%, " % (self.get_voting_power())])
|
|
439
|
+
t.add_row(["Downvoting Power", "%.2f %%, " % (self.get_downvoting_power())])
|
|
440
|
+
t.add_row(["Vote Value", "%.2f $" % (self.get_voting_value_SBD())])
|
|
441
|
+
t.add_row(["Last vote", "%s ago" % last_vote_time_str])
|
|
442
|
+
t.add_row(["Full in ", "%s" % (self.get_recharge_time_str())])
|
|
443
|
+
t.add_row(
|
|
444
|
+
["Token Power", "%.2f %s" % (self.get_token_power(), self.blockchain.token_symbol)]
|
|
445
|
+
)
|
|
446
|
+
t.add_row(
|
|
447
|
+
[
|
|
448
|
+
"Balance",
|
|
449
|
+
"%s, %s"
|
|
450
|
+
% (str(self.balances["available"][0]), str(self.balances["available"][1])),
|
|
451
|
+
]
|
|
452
|
+
)
|
|
453
|
+
if (
|
|
454
|
+
False
|
|
455
|
+
and bandwidth is not None
|
|
456
|
+
and bandwidth["allocated"] is not None
|
|
457
|
+
and bandwidth["allocated"] > 0
|
|
458
|
+
):
|
|
459
|
+
t.add_row(["Remaining Bandwidth", "%.2f %%" % (remaining)])
|
|
460
|
+
t.add_row(
|
|
461
|
+
["used/allocated Bandwidth", "(%.0f kb of %.0f mb)" % (used_kb, allocated_mb)]
|
|
462
|
+
)
|
|
463
|
+
if rc_mana is not None:
|
|
464
|
+
estimated_rc = int(rc["max_rc"]) * rc_mana["current_pct"] / 100
|
|
465
|
+
t.add_row(["Remaining RC", "%.2f %%" % (rc_mana["current_pct"])])
|
|
466
|
+
t.add_row(
|
|
467
|
+
[
|
|
468
|
+
"Remaining RC",
|
|
469
|
+
"(%.0f G RC of %.0f G RC)"
|
|
470
|
+
% (estimated_rc / 10**9, int(rc["max_rc"]) / 10**9),
|
|
471
|
+
]
|
|
472
|
+
)
|
|
473
|
+
t.add_row(["Full in ", "%s" % (self.get_manabar_recharge_time_str(rc_mana))])
|
|
474
|
+
t.add_row(["Est. RC for a comment", "%.2f G RC" % (rc_calc.comment() / 10**9)])
|
|
475
|
+
t.add_row(["Est. RC for a vote", "%.2f G RC" % (rc_calc.vote() / 10**9)])
|
|
476
|
+
t.add_row(["Est. RC for a transfer", "%.2f G RC" % (rc_calc.transfer() / 10**9)])
|
|
477
|
+
t.add_row(
|
|
478
|
+
["Est. RC for a custom_json", "%.2f G RC" % (rc_calc.custom_json() / 10**9)]
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
t.add_row(
|
|
482
|
+
[
|
|
483
|
+
"Comments with current RC",
|
|
484
|
+
"%d comments" % (int(estimated_rc / rc_calc.comment())),
|
|
485
|
+
]
|
|
486
|
+
)
|
|
487
|
+
t.add_row(
|
|
488
|
+
["Votes with current RC", "%d votes" % (int(estimated_rc / rc_calc.vote()))]
|
|
489
|
+
)
|
|
490
|
+
t.add_row(
|
|
491
|
+
[
|
|
492
|
+
"Transfer with current RC",
|
|
493
|
+
"%d transfers" % (int(estimated_rc / rc_calc.transfer())),
|
|
494
|
+
]
|
|
495
|
+
)
|
|
496
|
+
t.add_row(
|
|
497
|
+
[
|
|
498
|
+
"Custom_json with current RC",
|
|
499
|
+
"%d transfers" % (int(estimated_rc / rc_calc.custom_json())),
|
|
500
|
+
]
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
if return_str:
|
|
504
|
+
return t.get_string(**kwargs)
|
|
505
|
+
else:
|
|
506
|
+
print(t.get_string(**kwargs))
|
|
507
|
+
else:
|
|
508
|
+
ret = self.name + " (%.2f) \n" % (self.rep)
|
|
509
|
+
ret += "--- Voting Power ---\n"
|
|
510
|
+
ret += "%.2f %%, " % (self.get_voting_power())
|
|
511
|
+
ret += " %.2f $\n" % (self.get_voting_value_SBD())
|
|
512
|
+
ret += "full in %s \n" % (self.get_recharge_time_str())
|
|
513
|
+
ret += "--- Downvoting Power ---\n"
|
|
514
|
+
ret += "%.2f %% \n" % (self.get_downvoting_power())
|
|
515
|
+
ret += "--- Balance ---\n"
|
|
516
|
+
ret += "%.2f SP, " % (self.get_token_power())
|
|
517
|
+
ret += "%s, %s\n" % (
|
|
518
|
+
str(self.balances["available"][0]),
|
|
519
|
+
str(self.balances["available"][1]),
|
|
520
|
+
)
|
|
521
|
+
if False and bandwidth["allocated"] > 0:
|
|
522
|
+
ret += "--- Bandwidth ---\n"
|
|
523
|
+
ret += "Remaining: %.2f %%" % (remaining)
|
|
524
|
+
ret += " (%.0f kb of %.0f mb)\n" % (used_kb, allocated_mb)
|
|
525
|
+
if rc_mana is not None:
|
|
526
|
+
estimated_rc = int(rc["max_rc"]) * rc_mana["current_pct"] / 100
|
|
527
|
+
ret += "--- RC manabar ---\n"
|
|
528
|
+
ret += "Remaining: %.2f %%" % (rc_mana["current_pct"])
|
|
529
|
+
ret += " (%.0f G RC of %.0f G RC)\n" % (
|
|
530
|
+
estimated_rc / 10**9,
|
|
531
|
+
int(rc["max_rc"]) / 10**9,
|
|
532
|
+
)
|
|
533
|
+
ret += "full in %s\n" % (self.get_manabar_recharge_time_str(rc_mana))
|
|
534
|
+
ret += "--- Approx Costs ---\n"
|
|
535
|
+
ret += "comment - %.2f G RC - enough RC for %d comments\n" % (
|
|
536
|
+
rc_calc.comment() / 10**9,
|
|
537
|
+
int(estimated_rc / rc_calc.comment()),
|
|
538
|
+
)
|
|
539
|
+
ret += "vote - %.2f G RC - enough RC for %d votes\n" % (
|
|
540
|
+
rc_calc.vote() / 10**9,
|
|
541
|
+
int(estimated_rc / rc_calc.vote()),
|
|
542
|
+
)
|
|
543
|
+
ret += "transfer - %.2f G RC - enough RC for %d transfers\n" % (
|
|
544
|
+
rc_calc.transfer() / 10**9,
|
|
545
|
+
int(estimated_rc / rc_calc.transfer()),
|
|
546
|
+
)
|
|
547
|
+
ret += "custom_json - %.2f G RC - enough RC for %d custom_json\n" % (
|
|
548
|
+
rc_calc.custom_json() / 10**9,
|
|
549
|
+
int(estimated_rc / rc_calc.custom_json()),
|
|
550
|
+
)
|
|
551
|
+
if return_str:
|
|
552
|
+
return ret
|
|
553
|
+
print(ret)
|
|
554
|
+
|
|
555
|
+
def get_reputation(self):
|
|
556
|
+
"""Returns the account reputation in the (steemit) normalized form"""
|
|
557
|
+
if not self.blockchain.is_connected():
|
|
558
|
+
return None
|
|
559
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
560
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
561
|
+
try:
|
|
562
|
+
rep = self.blockchain.rpc.get_account_reputations(
|
|
563
|
+
{"account_lower_bound": self["name"], "limit": 1}, api="reputation"
|
|
564
|
+
)["reputations"]
|
|
565
|
+
if len(rep) > 0:
|
|
566
|
+
rep = int(rep[0]["reputation"])
|
|
567
|
+
except:
|
|
568
|
+
if "reputation" in self:
|
|
569
|
+
rep = int(self["reputation"])
|
|
570
|
+
else:
|
|
571
|
+
rep = 0
|
|
572
|
+
else:
|
|
573
|
+
rep = int(self["reputation"])
|
|
574
|
+
return reputation_to_score(rep)
|
|
575
|
+
|
|
576
|
+
def get_manabar(self):
|
|
577
|
+
"""Return manabar"""
|
|
578
|
+
max_mana = self.get_effective_vesting_shares()
|
|
579
|
+
if max_mana == 0:
|
|
580
|
+
props = self.blockchain.get_chain_properties()
|
|
581
|
+
required_fee_token = Amount(
|
|
582
|
+
props["account_creation_fee"], blockchain_instance=self.blockchain
|
|
583
|
+
)
|
|
584
|
+
max_mana = int(self.blockchain.token_power_to_vests(required_fee_token))
|
|
585
|
+
|
|
586
|
+
last_mana = int(self["voting_manabar"]["current_mana"])
|
|
587
|
+
last_update_time = self["voting_manabar"]["last_update_time"]
|
|
588
|
+
last_update = datetime.fromtimestamp(last_update_time, tz=timezone.utc)
|
|
589
|
+
diff_in_seconds = (
|
|
590
|
+
addTzInfo(datetime.now(timezone.utc)) - addTzInfo(last_update)
|
|
591
|
+
).total_seconds()
|
|
592
|
+
current_mana = int(
|
|
593
|
+
last_mana + diff_in_seconds * max_mana / STEEM_VOTING_MANA_REGENERATION_SECONDS
|
|
594
|
+
)
|
|
595
|
+
if current_mana > max_mana:
|
|
596
|
+
current_mana = max_mana
|
|
597
|
+
if max_mana > 0:
|
|
598
|
+
current_mana_pct = current_mana / max_mana * 100
|
|
599
|
+
else:
|
|
600
|
+
current_mana_pct = 0
|
|
601
|
+
return {
|
|
602
|
+
"last_mana": last_mana,
|
|
603
|
+
"last_update_time": last_update_time,
|
|
604
|
+
"current_mana": current_mana,
|
|
605
|
+
"max_mana": max_mana,
|
|
606
|
+
"current_mana_pct": current_mana_pct,
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
def get_downvote_manabar(self):
|
|
610
|
+
"""Return downvote manabar"""
|
|
611
|
+
if "downvote_manabar" not in self:
|
|
612
|
+
return None
|
|
613
|
+
max_mana = self.get_effective_vesting_shares() / 4
|
|
614
|
+
if max_mana == 0:
|
|
615
|
+
props = self.blockchain.get_chain_properties()
|
|
616
|
+
required_fee_token = Amount(
|
|
617
|
+
props["account_creation_fee"], blockchain_instance=self.blockchain
|
|
618
|
+
)
|
|
619
|
+
max_mana = int(self.blockchain.token_power_to_vests(required_fee_token) / 4)
|
|
620
|
+
|
|
621
|
+
last_mana = int(self["downvote_manabar"]["current_mana"])
|
|
622
|
+
last_update_time = self["downvote_manabar"]["last_update_time"]
|
|
623
|
+
last_update = datetime.fromtimestamp(last_update_time, tz=timezone.utc)
|
|
624
|
+
diff_in_seconds = (
|
|
625
|
+
addTzInfo(datetime.now(timezone.utc)) - addTzInfo(last_update)
|
|
626
|
+
).total_seconds()
|
|
627
|
+
current_mana = int(
|
|
628
|
+
last_mana + diff_in_seconds * max_mana / STEEM_VOTING_MANA_REGENERATION_SECONDS
|
|
629
|
+
)
|
|
630
|
+
if current_mana > max_mana:
|
|
631
|
+
current_mana = max_mana
|
|
632
|
+
if max_mana > 0:
|
|
633
|
+
current_mana_pct = current_mana / max_mana * 100
|
|
634
|
+
else:
|
|
635
|
+
current_mana_pct = 0
|
|
636
|
+
return {
|
|
637
|
+
"last_mana": last_mana,
|
|
638
|
+
"last_update_time": last_update_time,
|
|
639
|
+
"current_mana": current_mana,
|
|
640
|
+
"max_mana": max_mana,
|
|
641
|
+
"current_mana_pct": current_mana_pct,
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
def get_voting_power(self, with_regeneration=True):
|
|
645
|
+
"""Returns the account voting power in the range of 0-100%
|
|
646
|
+
|
|
647
|
+
:param bool with_regeneration: When True, voting power regeneration is
|
|
648
|
+
included into the result (default True)
|
|
649
|
+
"""
|
|
650
|
+
if "voting_manabar" in self:
|
|
651
|
+
manabar = self.get_manabar()
|
|
652
|
+
if with_regeneration:
|
|
653
|
+
total_vp = manabar["current_mana_pct"]
|
|
654
|
+
else:
|
|
655
|
+
if manabar["max_mana"] > 0:
|
|
656
|
+
total_vp = manabar["last_mana"] / manabar["max_mana"] * 100
|
|
657
|
+
else:
|
|
658
|
+
total_vp = 0
|
|
659
|
+
elif "voting_power" in self:
|
|
660
|
+
if with_regeneration:
|
|
661
|
+
last_vote_time = self["last_vote_time"]
|
|
662
|
+
diff_in_seconds = (
|
|
663
|
+
addTzInfo(datetime.now(timezone.utc)) - (last_vote_time)
|
|
664
|
+
).total_seconds()
|
|
665
|
+
regenerated_vp = (
|
|
666
|
+
diff_in_seconds * STEEM_100_PERCENT / STEEM_VOTE_REGENERATION_SECONDS / 100
|
|
667
|
+
)
|
|
668
|
+
else:
|
|
669
|
+
regenerated_vp = 0
|
|
670
|
+
total_vp = self["voting_power"] / 100 + regenerated_vp
|
|
671
|
+
if total_vp > 100:
|
|
672
|
+
return 100
|
|
673
|
+
if total_vp < 0:
|
|
674
|
+
return 0
|
|
675
|
+
return total_vp
|
|
676
|
+
|
|
677
|
+
def get_downvoting_power(self, with_regeneration=True):
|
|
678
|
+
"""Returns the account downvoting power in the range of 0-100%
|
|
679
|
+
|
|
680
|
+
:param bool with_regeneration: When True, downvoting power regeneration is
|
|
681
|
+
included into the result (default True)
|
|
682
|
+
"""
|
|
683
|
+
if "downvote_manabar" not in self:
|
|
684
|
+
return 0
|
|
685
|
+
|
|
686
|
+
manabar = self.get_downvote_manabar()
|
|
687
|
+
if with_regeneration:
|
|
688
|
+
total_down_vp = manabar["current_mana_pct"]
|
|
689
|
+
else:
|
|
690
|
+
if manabar["max_mana"] > 0:
|
|
691
|
+
total_down_vp = manabar["last_mana"] / manabar["max_mana"] * 100
|
|
692
|
+
else:
|
|
693
|
+
total_down_vp = 0
|
|
694
|
+
if total_down_vp > 100:
|
|
695
|
+
return 100
|
|
696
|
+
if total_down_vp < 0:
|
|
697
|
+
return 0
|
|
698
|
+
return total_down_vp
|
|
699
|
+
|
|
700
|
+
def get_vests(self, only_own_vests=False):
|
|
701
|
+
"""Returns the account vests
|
|
702
|
+
|
|
703
|
+
:param bool only_own_vests: When True, only owned vests is
|
|
704
|
+
returned without delegation (default False)
|
|
705
|
+
"""
|
|
706
|
+
vests = self["vesting_shares"]
|
|
707
|
+
if (
|
|
708
|
+
not only_own_vests
|
|
709
|
+
and "delegated_vesting_shares" in self
|
|
710
|
+
and "received_vesting_shares" in self
|
|
711
|
+
):
|
|
712
|
+
vests = vests - (self["delegated_vesting_shares"]) + (self["received_vesting_shares"])
|
|
713
|
+
|
|
714
|
+
return vests
|
|
715
|
+
|
|
716
|
+
def get_effective_vesting_shares(self):
|
|
717
|
+
"""Returns the effective vesting shares"""
|
|
718
|
+
vesting_shares = int(self["vesting_shares"])
|
|
719
|
+
if "delegated_vesting_shares" in self and "received_vesting_shares" in self:
|
|
720
|
+
vesting_shares = (
|
|
721
|
+
vesting_shares
|
|
722
|
+
- int(self["delegated_vesting_shares"])
|
|
723
|
+
+ int(self["received_vesting_shares"])
|
|
724
|
+
)
|
|
725
|
+
timestamp = (
|
|
726
|
+
self["next_vesting_withdrawal"] - addTzInfo(datetime(1970, 1, 1))
|
|
727
|
+
).total_seconds()
|
|
728
|
+
if (
|
|
729
|
+
timestamp > 0
|
|
730
|
+
and "vesting_withdraw_rate" in self
|
|
731
|
+
and "to_withdraw" in self
|
|
732
|
+
and "withdrawn" in self
|
|
733
|
+
):
|
|
734
|
+
vesting_shares -= min(
|
|
735
|
+
int(self["vesting_withdraw_rate"]),
|
|
736
|
+
int(self["to_withdraw"]) - int(self["withdrawn"]),
|
|
737
|
+
)
|
|
738
|
+
return vesting_shares
|
|
739
|
+
|
|
740
|
+
def get_token_power(self, only_own_vests=False, use_stored_data=True):
|
|
741
|
+
"""Returns the account Hive/Steem power (amount of staked token + delegations)
|
|
742
|
+
|
|
743
|
+
:param bool only_own_vests: When True, only owned vests is
|
|
744
|
+
returned without delegation (default False)
|
|
745
|
+
:param bool use_stored_data: When False, an API call returns the current
|
|
746
|
+
vests_to_token_power ratio everytime (default True)
|
|
747
|
+
|
|
748
|
+
"""
|
|
749
|
+
return self.blockchain.vests_to_token_power(
|
|
750
|
+
self.get_vests(only_own_vests=only_own_vests), use_stored_data=use_stored_data
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
def get_steem_power(self, onlyOwnSP=False):
|
|
754
|
+
"""Returns the account steem power"""
|
|
755
|
+
return self.get_token_power(only_own_vests=onlyOwnSP)
|
|
756
|
+
|
|
757
|
+
def get_voting_value(
|
|
758
|
+
self,
|
|
759
|
+
post_rshares=0,
|
|
760
|
+
voting_weight=100,
|
|
761
|
+
voting_power=None,
|
|
762
|
+
token_power=None,
|
|
763
|
+
not_broadcasted_vote=True,
|
|
764
|
+
):
|
|
765
|
+
"""Returns the account voting value in Hive/Steem token units"""
|
|
766
|
+
if voting_power is None:
|
|
767
|
+
voting_power = self.get_voting_power()
|
|
768
|
+
if token_power is None:
|
|
769
|
+
tp = self.get_token_power()
|
|
770
|
+
else:
|
|
771
|
+
tp = token_power
|
|
772
|
+
voteValue = self.blockchain.token_power_to_token_backed_dollar(
|
|
773
|
+
tp,
|
|
774
|
+
post_rshares=post_rshares,
|
|
775
|
+
voting_power=voting_power * 100,
|
|
776
|
+
vote_pct=voting_weight * 100,
|
|
777
|
+
not_broadcasted_vote=not_broadcasted_vote,
|
|
778
|
+
)
|
|
779
|
+
return voteValue
|
|
780
|
+
|
|
781
|
+
def get_voting_value_SBD(
|
|
782
|
+
self,
|
|
783
|
+
post_rshares=0,
|
|
784
|
+
voting_weight=100,
|
|
785
|
+
voting_power=None,
|
|
786
|
+
steem_power=None,
|
|
787
|
+
not_broadcasted_vote=True,
|
|
788
|
+
):
|
|
789
|
+
"""Returns the account voting value in SBD"""
|
|
790
|
+
return self.get_voting_value(
|
|
791
|
+
post_rshares=post_rshares,
|
|
792
|
+
voting_weight=voting_weight,
|
|
793
|
+
voting_power=voting_power,
|
|
794
|
+
token_power=steem_power,
|
|
795
|
+
not_broadcasted_vote=not_broadcasted_vote,
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
def get_vote_pct_for_SBD(
|
|
799
|
+
self, sbd, post_rshares=0, voting_power=None, steem_power=None, not_broadcasted_vote=True
|
|
800
|
+
):
|
|
801
|
+
"""Returns the voting percentage needed to have a vote worth a given number of SBD.
|
|
802
|
+
|
|
803
|
+
If the returned number is bigger than 10000 or smaller than -10000,
|
|
804
|
+
the given SBD value is too high for that account
|
|
805
|
+
|
|
806
|
+
:param sbd: The amount of SBD in vote value
|
|
807
|
+
:type sbd: str, int, amount.Amount
|
|
808
|
+
|
|
809
|
+
"""
|
|
810
|
+
return self.get_vote_pct_for_vote_value(
|
|
811
|
+
sbd,
|
|
812
|
+
post_rshares=post_rshares,
|
|
813
|
+
voting_power=voting_power,
|
|
814
|
+
token_power=steem_power,
|
|
815
|
+
not_broadcasted_vote=not_broadcasted_vote,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
def get_vote_pct_for_vote_value(
|
|
819
|
+
self,
|
|
820
|
+
token_units,
|
|
821
|
+
post_rshares=0,
|
|
822
|
+
voting_power=None,
|
|
823
|
+
token_power=None,
|
|
824
|
+
not_broadcasted_vote=True,
|
|
825
|
+
):
|
|
826
|
+
"""Returns the voting percentage needed to have a vote worth a given number of Hive/Steem token units
|
|
827
|
+
|
|
828
|
+
If the returned number is bigger than 10000 or smaller than -10000,
|
|
829
|
+
the given SBD value is too high for that account
|
|
830
|
+
|
|
831
|
+
:param token_units: The amount of HBD/SBD in vote value
|
|
832
|
+
:type token_units: str, int, amount.Amount
|
|
833
|
+
|
|
834
|
+
"""
|
|
835
|
+
if voting_power is None:
|
|
836
|
+
voting_power = self.get_voting_power()
|
|
837
|
+
if token_power is None:
|
|
838
|
+
token_power = self.get_token_power()
|
|
839
|
+
|
|
840
|
+
if isinstance(token_units, Amount):
|
|
841
|
+
token_units = Amount(token_units, blockchain_instance=self.blockchain)
|
|
842
|
+
elif isinstance(token_units, string_types):
|
|
843
|
+
token_units = Amount(token_units, blockchain_instance=self.blockchain)
|
|
844
|
+
else:
|
|
845
|
+
token_units = Amount(
|
|
846
|
+
token_units,
|
|
847
|
+
self.blockchain.backed_token_symbol,
|
|
848
|
+
blockchain_instance=self.blockchain,
|
|
849
|
+
)
|
|
850
|
+
if token_units["symbol"] != self.blockchain.backed_token_symbol:
|
|
851
|
+
raise AssertionError(
|
|
852
|
+
"Should input %s, not any other asset!" % self.blockchain.backed_token_symbol
|
|
853
|
+
)
|
|
854
|
+
from nectar import Steem
|
|
855
|
+
|
|
856
|
+
if isinstance(self.blockchain, Steem):
|
|
857
|
+
vote_pct = self.blockchain.rshares_to_vote_pct(
|
|
858
|
+
self.blockchain.sbd_to_rshares(
|
|
859
|
+
token_units, not_broadcasted_vote=not_broadcasted_vote
|
|
860
|
+
),
|
|
861
|
+
post_rshares=post_rshares,
|
|
862
|
+
voting_power=voting_power * 100,
|
|
863
|
+
steem_power=token_power,
|
|
864
|
+
)
|
|
865
|
+
else:
|
|
866
|
+
vote_pct = self.blockchain.rshares_to_vote_pct(
|
|
867
|
+
self.blockchain.hbd_to_rshares(
|
|
868
|
+
token_units, not_broadcasted_vote=not_broadcasted_vote
|
|
869
|
+
),
|
|
870
|
+
post_rshares=post_rshares,
|
|
871
|
+
voting_power=voting_power * 100,
|
|
872
|
+
hive_power=token_power,
|
|
873
|
+
)
|
|
874
|
+
return vote_pct
|
|
875
|
+
|
|
876
|
+
def get_creator(self):
|
|
877
|
+
"""Returns the account creator or `None` if the account was mined"""
|
|
878
|
+
if self["mined"]:
|
|
879
|
+
return None
|
|
880
|
+
ops = list(self.get_account_history(1, 1))
|
|
881
|
+
if not ops or "creator" not in ops[-1]:
|
|
882
|
+
return None
|
|
883
|
+
return ops[-1]["creator"]
|
|
884
|
+
|
|
885
|
+
def get_recharge_time_str(self, voting_power_goal=100, starting_voting_power=None):
|
|
886
|
+
"""Returns the account recharge time as string
|
|
887
|
+
|
|
888
|
+
:param float voting_power_goal: voting power goal in percentage (default is 100)
|
|
889
|
+
:param float starting_voting_power: returns recharge time if current voting power is
|
|
890
|
+
the provided value.
|
|
891
|
+
|
|
892
|
+
"""
|
|
893
|
+
remainingTime = self.get_recharge_timedelta(
|
|
894
|
+
voting_power_goal=voting_power_goal, starting_voting_power=starting_voting_power
|
|
895
|
+
)
|
|
896
|
+
return formatTimedelta(remainingTime)
|
|
897
|
+
|
|
898
|
+
def get_recharge_timedelta(self, voting_power_goal=100, starting_voting_power=None):
|
|
899
|
+
"""Returns the account voting power recharge time as timedelta object
|
|
900
|
+
|
|
901
|
+
:param float voting_power_goal: voting power goal in percentage (default is 100)
|
|
902
|
+
:param float starting_voting_power: returns recharge time if current voting power is
|
|
903
|
+
the provided value.
|
|
904
|
+
|
|
905
|
+
"""
|
|
906
|
+
if starting_voting_power is None:
|
|
907
|
+
missing_vp = voting_power_goal - self.get_voting_power()
|
|
908
|
+
elif isinstance(starting_voting_power, int) or isinstance(starting_voting_power, float):
|
|
909
|
+
missing_vp = voting_power_goal - starting_voting_power
|
|
910
|
+
else:
|
|
911
|
+
raise ValueError("starting_voting_power must be a number.")
|
|
912
|
+
if missing_vp < 0:
|
|
913
|
+
return 0
|
|
914
|
+
recharge_seconds = (
|
|
915
|
+
missing_vp * 100 * STEEM_VOTING_MANA_REGENERATION_SECONDS / STEEM_100_PERCENT
|
|
916
|
+
)
|
|
917
|
+
return timedelta(seconds=recharge_seconds)
|
|
918
|
+
|
|
919
|
+
def get_recharge_time(self, voting_power_goal=100, starting_voting_power=None):
|
|
920
|
+
"""Returns the account voting power recharge time in minutes
|
|
921
|
+
|
|
922
|
+
:param float voting_power_goal: voting power goal in percentage (default is 100)
|
|
923
|
+
:param float starting_voting_power: returns recharge time if current voting power is
|
|
924
|
+
the provided value.
|
|
925
|
+
|
|
926
|
+
"""
|
|
927
|
+
return addTzInfo(datetime.now(timezone.utc)) + self.get_recharge_timedelta(
|
|
928
|
+
voting_power_goal, starting_voting_power
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
def get_manabar_recharge_time_str(self, manabar, recharge_pct_goal=100):
|
|
932
|
+
"""Returns the account manabar recharge time as string
|
|
933
|
+
|
|
934
|
+
:param dict manabar: manabar dict from get_manabar() or get_rc_manabar()
|
|
935
|
+
:param float recharge_pct_goal: mana recovery goal in percentage (default is 100)
|
|
936
|
+
|
|
937
|
+
"""
|
|
938
|
+
remainingTime = self.get_manabar_recharge_timedelta(
|
|
939
|
+
manabar, recharge_pct_goal=recharge_pct_goal
|
|
940
|
+
)
|
|
941
|
+
return formatTimedelta(remainingTime)
|
|
942
|
+
|
|
943
|
+
def get_manabar_recharge_timedelta(self, manabar, recharge_pct_goal=100):
|
|
944
|
+
"""Returns the account mana recharge time as timedelta object
|
|
945
|
+
|
|
946
|
+
:param dict manabar: manabar dict from get_manabar() or get_rc_manabar()
|
|
947
|
+
:param float recharge_pct_goal: mana recovery goal in percentage (default is 100)
|
|
948
|
+
|
|
949
|
+
"""
|
|
950
|
+
if "current_mana_pct" in manabar:
|
|
951
|
+
missing_rc_pct = recharge_pct_goal - manabar["current_mana_pct"]
|
|
952
|
+
else:
|
|
953
|
+
missing_rc_pct = recharge_pct_goal - manabar["current_pct"]
|
|
954
|
+
if missing_rc_pct < 0:
|
|
955
|
+
return 0
|
|
956
|
+
recharge_seconds = (
|
|
957
|
+
missing_rc_pct * 100 * STEEM_VOTING_MANA_REGENERATION_SECONDS / STEEM_100_PERCENT
|
|
958
|
+
)
|
|
959
|
+
return timedelta(seconds=recharge_seconds)
|
|
960
|
+
|
|
961
|
+
def get_manabar_recharge_time(self, manabar, recharge_pct_goal=100):
|
|
962
|
+
"""Returns the account mana recharge time in minutes
|
|
963
|
+
|
|
964
|
+
:param dict manabar: manabar dict from get_manabar() or get_rc_manabar()
|
|
965
|
+
:param float recharge_pct_goal: mana recovery goal in percentage (default is 100)
|
|
966
|
+
|
|
967
|
+
"""
|
|
968
|
+
return addTzInfo(datetime.now(timezone.utc)) + self.get_manabar_recharge_timedelta(
|
|
969
|
+
manabar, recharge_pct_goal
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
def get_feed(
|
|
973
|
+
self, start_entry_id=0, limit=100, raw_data=False, short_entries=False, account=None
|
|
974
|
+
):
|
|
975
|
+
"""Returns a list of items in an account’s feed
|
|
976
|
+
|
|
977
|
+
:param int start_entry_id: default is 0
|
|
978
|
+
:param int limit: default is 100
|
|
979
|
+
:param bool raw_data: default is False
|
|
980
|
+
:param bool short_entries: when set to True and raw_data is True, get_feed_entries is used istead of get_feed
|
|
981
|
+
:param str account: When set, a different account name is used (Default is object account name)
|
|
982
|
+
|
|
983
|
+
:rtype: list
|
|
984
|
+
|
|
985
|
+
.. code-block:: python
|
|
986
|
+
|
|
987
|
+
>>> from nectar.account import Account
|
|
988
|
+
>>> from nectar import Hive
|
|
989
|
+
>>> from nectar.nodelist import NodeList
|
|
990
|
+
>>> nodelist = NodeList()
|
|
991
|
+
>>> nodelist.update_nodes()
|
|
992
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
993
|
+
>>> account = Account("steemit", blockchain_instance=stm)
|
|
994
|
+
>>> account.get_feed(0, 1, raw_data=True)
|
|
995
|
+
[]
|
|
996
|
+
|
|
997
|
+
"""
|
|
998
|
+
if account is None:
|
|
999
|
+
account = self["name"]
|
|
1000
|
+
account = extract_account_name(account)
|
|
1001
|
+
if not self.blockchain.is_connected():
|
|
1002
|
+
return None
|
|
1003
|
+
from nectar.discussions import Discussions, Query
|
|
1004
|
+
|
|
1005
|
+
d = Discussions(blockchain_instance=self.blockchain)
|
|
1006
|
+
if short_entries:
|
|
1007
|
+
truncate_body = 1
|
|
1008
|
+
else:
|
|
1009
|
+
truncate_body = 0
|
|
1010
|
+
q = Query(limit=limit, tag=account, truncate_body=truncate_body)
|
|
1011
|
+
return [c for c in d.get_discussions("feed", q, limit=limit, raw_data=raw_data)]
|
|
1012
|
+
|
|
1013
|
+
def get_feed_entries(self, start_entry_id=0, limit=100, raw_data=True, account=None):
|
|
1014
|
+
"""Returns a list of entries in an account’s feed
|
|
1015
|
+
|
|
1016
|
+
:param int start_entry_id: default is 0
|
|
1017
|
+
:param int limit: default is 100
|
|
1018
|
+
:param bool raw_data: default is False
|
|
1019
|
+
:param bool short_entries: when set to True and raw_data is True, get_feed_entries is used istead of get_feed
|
|
1020
|
+
:param str account: When set, a different account name is used (Default is object account name)
|
|
1021
|
+
|
|
1022
|
+
:rtype: list
|
|
1023
|
+
|
|
1024
|
+
.. code-block:: python
|
|
1025
|
+
|
|
1026
|
+
>>> from nectar.account import Account
|
|
1027
|
+
>>> from nectar import Hive
|
|
1028
|
+
>>> from nectar.nodelist import NodeList
|
|
1029
|
+
>>> nodelist = NodeList()
|
|
1030
|
+
>>> nodelist.update_nodes()
|
|
1031
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1032
|
+
>>> account = Account("steemit", blockchain_instance=stm)
|
|
1033
|
+
>>> account.get_feed_entries(0, 1)
|
|
1034
|
+
[]
|
|
1035
|
+
|
|
1036
|
+
"""
|
|
1037
|
+
return self.get_feed(
|
|
1038
|
+
start_entry_id=start_entry_id,
|
|
1039
|
+
limit=limit,
|
|
1040
|
+
raw_data=raw_data,
|
|
1041
|
+
short_entries=True,
|
|
1042
|
+
account=account,
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
def get_blog_entries(self, start_entry_id=0, limit=100, raw_data=True, account=None):
|
|
1046
|
+
"""Returns the list of blog entries for an account
|
|
1047
|
+
|
|
1048
|
+
:param int start_entry_id: default is 0
|
|
1049
|
+
:param int limit: default is 100
|
|
1050
|
+
:param bool raw_data: default is False
|
|
1051
|
+
:param str account: When set, a different account name is used (Default is object account name)
|
|
1052
|
+
|
|
1053
|
+
:rtype: list
|
|
1054
|
+
|
|
1055
|
+
.. code-block:: python
|
|
1056
|
+
|
|
1057
|
+
>>> from nectar.account import Account
|
|
1058
|
+
>>> from nectar import Hive
|
|
1059
|
+
>>> from nectar.nodelist import NodeList
|
|
1060
|
+
>>> nodelist = NodeList()
|
|
1061
|
+
>>> nodelist.update_nodes()
|
|
1062
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1063
|
+
>>> account = Account("steemit", blockchain_instance=stm)
|
|
1064
|
+
>>> entry = account.get_blog_entries(0, 1, raw_data=True)[0]
|
|
1065
|
+
>>> print("%s - %s - %s" % (entry["author"], entry["permlink"], entry["blog"]))
|
|
1066
|
+
steemit - firstpost - steemit
|
|
1067
|
+
|
|
1068
|
+
"""
|
|
1069
|
+
return self.get_blog(
|
|
1070
|
+
start_entry_id=start_entry_id,
|
|
1071
|
+
limit=limit,
|
|
1072
|
+
raw_data=raw_data,
|
|
1073
|
+
short_entries=True,
|
|
1074
|
+
account=account,
|
|
1075
|
+
)
|
|
1076
|
+
|
|
1077
|
+
def get_blog(
|
|
1078
|
+
self, start_entry_id=0, limit=100, raw_data=False, short_entries=False, account=None
|
|
1079
|
+
):
|
|
1080
|
+
"""Returns the list of blog entries for an account
|
|
1081
|
+
|
|
1082
|
+
:param int start_entry_id: default is 0
|
|
1083
|
+
:param int limit: default is 100
|
|
1084
|
+
:param bool raw_data: default is False
|
|
1085
|
+
:param bool short_entries: when set to True and raw_data is True, get_blog_entries is used istead of get_blog
|
|
1086
|
+
:param str account: When set, a different account name is used (Default is object account name)
|
|
1087
|
+
|
|
1088
|
+
:rtype: list
|
|
1089
|
+
|
|
1090
|
+
.. code-block:: python
|
|
1091
|
+
|
|
1092
|
+
>>> from nectar.account import Account
|
|
1093
|
+
>>> from nectar import Hive
|
|
1094
|
+
>>> from nectar.nodelist import NodeList
|
|
1095
|
+
>>> nodelist = NodeList()
|
|
1096
|
+
>>> nodelist.update_nodes()
|
|
1097
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1098
|
+
>>> account = Account("steemit", blockchain_instance=stm)
|
|
1099
|
+
>>> account.get_blog(0, 1)
|
|
1100
|
+
[<Comment @steemit/firstpost>]
|
|
1101
|
+
|
|
1102
|
+
"""
|
|
1103
|
+
if account is None:
|
|
1104
|
+
account = self["name"]
|
|
1105
|
+
account = extract_account_name(account)
|
|
1106
|
+
|
|
1107
|
+
if not self.blockchain.is_connected():
|
|
1108
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1109
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1110
|
+
success = True
|
|
1111
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1112
|
+
try:
|
|
1113
|
+
if raw_data and short_entries:
|
|
1114
|
+
ret = self.blockchain.rpc.get_blog_entries(
|
|
1115
|
+
{"account": account, "start_entry_id": start_entry_id, "limit": limit},
|
|
1116
|
+
api="condenser",
|
|
1117
|
+
)
|
|
1118
|
+
if isinstance(ret, dict) and "blog" in ret:
|
|
1119
|
+
ret = ret["blog"]
|
|
1120
|
+
return [c for c in ret]
|
|
1121
|
+
elif raw_data:
|
|
1122
|
+
ret = self.blockchain.rpc.get_blog(
|
|
1123
|
+
{"account": account, "start_entry_id": start_entry_id, "limit": limit},
|
|
1124
|
+
api="condenser",
|
|
1125
|
+
)
|
|
1126
|
+
if isinstance(ret, dict) and "blog" in ret:
|
|
1127
|
+
ret = ret["blog"]
|
|
1128
|
+
return [c for c in ret]
|
|
1129
|
+
elif not raw_data:
|
|
1130
|
+
from .comment import Comment
|
|
1131
|
+
|
|
1132
|
+
ret = self.blockchain.rpc.get_blog(
|
|
1133
|
+
{"account": account, "start_entry_id": start_entry_id, "limit": limit},
|
|
1134
|
+
api="condenser",
|
|
1135
|
+
)
|
|
1136
|
+
if isinstance(ret, dict) and "blog" in ret:
|
|
1137
|
+
ret = ret["blog"]
|
|
1138
|
+
return [Comment(c["comment"], blockchain_instance=self.blockchain) for c in ret]
|
|
1139
|
+
except:
|
|
1140
|
+
success = False
|
|
1141
|
+
|
|
1142
|
+
if not self.blockchain.rpc.get_use_appbase() or not success:
|
|
1143
|
+
if raw_data and short_entries:
|
|
1144
|
+
return [
|
|
1145
|
+
c
|
|
1146
|
+
for c in self.blockchain.rpc.get_blog_entries(
|
|
1147
|
+
account, start_entry_id, limit, api="condenser"
|
|
1148
|
+
)
|
|
1149
|
+
]
|
|
1150
|
+
|
|
1151
|
+
elif raw_data:
|
|
1152
|
+
return [
|
|
1153
|
+
c
|
|
1154
|
+
for c in self.blockchain.rpc.get_blog(
|
|
1155
|
+
account, start_entry_id, limit, api="condenser"
|
|
1156
|
+
)
|
|
1157
|
+
]
|
|
1158
|
+
|
|
1159
|
+
else:
|
|
1160
|
+
from .comment import Comment
|
|
1161
|
+
|
|
1162
|
+
blog_list = self.blockchain.rpc.get_blog(
|
|
1163
|
+
account, start_entry_id, limit, api="condenser"
|
|
1164
|
+
)
|
|
1165
|
+
if blog_list is None:
|
|
1166
|
+
return []
|
|
1167
|
+
return [
|
|
1168
|
+
Comment(c["comment"], blockchain_instance=self.blockchain) for c in blog_list
|
|
1169
|
+
]
|
|
1170
|
+
|
|
1171
|
+
def get_notifications(self, only_unread=True, limit=100, raw_data=False, account=None):
|
|
1172
|
+
"""Returns account notifications
|
|
1173
|
+
|
|
1174
|
+
:param bool only_unread: When True, only unread notfications are shown
|
|
1175
|
+
:param int limit: When set, the number of shown notifications is limited (max limit = 100)
|
|
1176
|
+
:param bool raw_data: When True, the raw data from the api call is returned.
|
|
1177
|
+
:param str account: (optional) the account for which the notification should be received
|
|
1178
|
+
to (defaults to ``default_account``)
|
|
1179
|
+
"""
|
|
1180
|
+
if account is None:
|
|
1181
|
+
account = self["name"]
|
|
1182
|
+
account = extract_account_name(account)
|
|
1183
|
+
if not self.blockchain.is_connected():
|
|
1184
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1185
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1186
|
+
if only_unread:
|
|
1187
|
+
unread_notes = self.blockchain.rpc.unread_notifications(
|
|
1188
|
+
{"account": account}, api="bridge"
|
|
1189
|
+
)
|
|
1190
|
+
if limit is None or limit > unread_notes["unread"]:
|
|
1191
|
+
limit = unread_notes["unread"]
|
|
1192
|
+
if limit is None or limit == 0:
|
|
1193
|
+
return []
|
|
1194
|
+
if limit > 100:
|
|
1195
|
+
limit = 100
|
|
1196
|
+
notifications = self.blockchain.rpc.account_notifications(
|
|
1197
|
+
{"account": account, "limit": limit}, api="bridge"
|
|
1198
|
+
)
|
|
1199
|
+
if raw_data:
|
|
1200
|
+
return notifications
|
|
1201
|
+
ret = []
|
|
1202
|
+
for note in notifications:
|
|
1203
|
+
note["date"] = formatTimeString(note["date"])
|
|
1204
|
+
ret.append(note)
|
|
1205
|
+
return ret
|
|
1206
|
+
|
|
1207
|
+
def mark_notifications_as_read(self, last_read=None, account=None):
|
|
1208
|
+
"""Broadcast a mark all notification as read custom_json
|
|
1209
|
+
|
|
1210
|
+
:param str last_read: When set, this datestring is used to set the mark as read date
|
|
1211
|
+
:param str account: (optional) the account to broadcast the custom_json
|
|
1212
|
+
to (defaults to ``default_account``)
|
|
1213
|
+
|
|
1214
|
+
"""
|
|
1215
|
+
if account is None:
|
|
1216
|
+
account = self["name"]
|
|
1217
|
+
account = extract_account_name(account)
|
|
1218
|
+
if not account:
|
|
1219
|
+
raise ValueError("You need to provide an account")
|
|
1220
|
+
if last_read is None:
|
|
1221
|
+
last_notification = self.get_notifications(only_unread=False, limit=1, account=account)
|
|
1222
|
+
if len(last_notification) == 0:
|
|
1223
|
+
raise ValueError("Notification list is empty")
|
|
1224
|
+
last_read = last_notification[0]["date"]
|
|
1225
|
+
if isinstance(last_read, datetime):
|
|
1226
|
+
last_read = formatTimeString(last_read)
|
|
1227
|
+
json_body = [
|
|
1228
|
+
"setLastRead",
|
|
1229
|
+
{
|
|
1230
|
+
"date": last_read,
|
|
1231
|
+
},
|
|
1232
|
+
]
|
|
1233
|
+
return self.blockchain.custom_json("notify", json_body, required_posting_auths=[account])
|
|
1234
|
+
|
|
1235
|
+
def get_blog_authors(self, account=None):
|
|
1236
|
+
"""Returns a list of authors that have had their content reblogged on a given blog account
|
|
1237
|
+
|
|
1238
|
+
:param str account: When set, a different account name is used (Default is object account name)
|
|
1239
|
+
|
|
1240
|
+
:rtype: list
|
|
1241
|
+
|
|
1242
|
+
.. code-block:: python
|
|
1243
|
+
|
|
1244
|
+
>>> from nectar.account import Account
|
|
1245
|
+
>>> from nectar import Hive
|
|
1246
|
+
>>> from nectar.nodelist import NodeList
|
|
1247
|
+
>>> nodelist = NodeList()
|
|
1248
|
+
>>> nodelist.update_nodes()
|
|
1249
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1250
|
+
>>> account = Account("gtg", blockchain_instance=stm)
|
|
1251
|
+
>>> account.get_blog_authors() # doctest: +SKIP
|
|
1252
|
+
|
|
1253
|
+
"""
|
|
1254
|
+
if account is None:
|
|
1255
|
+
account = self["name"]
|
|
1256
|
+
account = extract_account_name(account)
|
|
1257
|
+
if not self.blockchain.is_connected():
|
|
1258
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1259
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1260
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1261
|
+
try:
|
|
1262
|
+
return self.blockchain.rpc.get_blog_authors(
|
|
1263
|
+
{"blog_account": account}, api="condenser"
|
|
1264
|
+
)["blog_authors"]
|
|
1265
|
+
except:
|
|
1266
|
+
return self.blockchain.rpc.get_blog_authors(account, api="condenser")
|
|
1267
|
+
else:
|
|
1268
|
+
return self.blockchain.rpc.get_blog_authors(account, api="condenser")
|
|
1269
|
+
|
|
1270
|
+
def get_follow_count(self, account=None):
|
|
1271
|
+
"""get_follow_count"""
|
|
1272
|
+
if account is None:
|
|
1273
|
+
account = self["name"]
|
|
1274
|
+
account = extract_account_name(account)
|
|
1275
|
+
if not self.blockchain.is_connected():
|
|
1276
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1277
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1278
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1279
|
+
try:
|
|
1280
|
+
return self.blockchain.rpc.get_follow_count({"account": account}, api="condenser")
|
|
1281
|
+
except:
|
|
1282
|
+
return self.blockchain.rpc.get_follow_count(account, api="condenser")
|
|
1283
|
+
else:
|
|
1284
|
+
return self.blockchain.rpc.get_follow_count(account, api="condenser")
|
|
1285
|
+
|
|
1286
|
+
def get_followers(self, raw_name_list=True, limit=100):
|
|
1287
|
+
"""Returns the account followers as list"""
|
|
1288
|
+
name_list = [x["follower"] for x in self._get_followers(direction="follower", limit=limit)]
|
|
1289
|
+
if raw_name_list:
|
|
1290
|
+
return name_list
|
|
1291
|
+
else:
|
|
1292
|
+
return Accounts(name_list, blockchain_instance=self.blockchain)
|
|
1293
|
+
|
|
1294
|
+
def get_following(self, raw_name_list=True, limit=100):
|
|
1295
|
+
"""Returns who the account is following as list"""
|
|
1296
|
+
name_list = [
|
|
1297
|
+
x["following"] for x in self._get_followers(direction="following", limit=limit)
|
|
1298
|
+
]
|
|
1299
|
+
if raw_name_list:
|
|
1300
|
+
return name_list
|
|
1301
|
+
else:
|
|
1302
|
+
return Accounts(name_list, blockchain_instance=self.blockchain)
|
|
1303
|
+
|
|
1304
|
+
def get_muters(self, raw_name_list=True, limit=100):
|
|
1305
|
+
"""Returns the account muters as list"""
|
|
1306
|
+
name_list = [
|
|
1307
|
+
x["follower"]
|
|
1308
|
+
for x in self._get_followers(direction="follower", what="ignore", limit=limit)
|
|
1309
|
+
]
|
|
1310
|
+
if raw_name_list:
|
|
1311
|
+
return name_list
|
|
1312
|
+
else:
|
|
1313
|
+
return Accounts(name_list, blockchain_instance=self.blockchain)
|
|
1314
|
+
|
|
1315
|
+
def get_mutings(self, raw_name_list=True, limit=100):
|
|
1316
|
+
"""Returns who the account is muting as list"""
|
|
1317
|
+
name_list = [
|
|
1318
|
+
x["following"]
|
|
1319
|
+
for x in self._get_followers(direction="following", what="ignore", limit=limit)
|
|
1320
|
+
]
|
|
1321
|
+
if raw_name_list:
|
|
1322
|
+
return name_list
|
|
1323
|
+
else:
|
|
1324
|
+
return Accounts(name_list, blockchain_instance=self.blockchain)
|
|
1325
|
+
|
|
1326
|
+
def get_follow_list(self, follow_type, starting_account=None, limit=100, raw_name_list=True):
|
|
1327
|
+
"""Returns the follow list for the specified follow_type (Only HIVE with HF >= 24)
|
|
1328
|
+
|
|
1329
|
+
:param list follow_type: follow_type can be `blacklisted`, `follow_blacklist` `muted`, or `follow_muted`
|
|
1330
|
+
"""
|
|
1331
|
+
if not self.blockchain.is_connected():
|
|
1332
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1333
|
+
limit_reached = True
|
|
1334
|
+
cnt = 0
|
|
1335
|
+
while limit_reached:
|
|
1336
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1337
|
+
query = {
|
|
1338
|
+
"observer": self.name,
|
|
1339
|
+
"follow_type": follow_type,
|
|
1340
|
+
"starting_account": starting_account,
|
|
1341
|
+
"limit": limit,
|
|
1342
|
+
}
|
|
1343
|
+
followers = self.blockchain.rpc.get_follow_list(query, api="bridge")
|
|
1344
|
+
if cnt == 0:
|
|
1345
|
+
name_list = followers
|
|
1346
|
+
elif followers is not None and len(followers) > 1:
|
|
1347
|
+
name_list += followers[1:]
|
|
1348
|
+
if followers is not None and len(followers) >= limit:
|
|
1349
|
+
starting_account = followers[-1]
|
|
1350
|
+
limit_reached = True
|
|
1351
|
+
cnt += 1
|
|
1352
|
+
else:
|
|
1353
|
+
limit_reached = False
|
|
1354
|
+
if raw_name_list:
|
|
1355
|
+
return name_list
|
|
1356
|
+
else:
|
|
1357
|
+
return Accounts(name_list, blockchain_instance=self.blockchain)
|
|
1358
|
+
|
|
1359
|
+
def _get_followers(self, direction="follower", last_user="", what="blog", limit=100):
|
|
1360
|
+
"""Help function, used in get_followers and get_following"""
|
|
1361
|
+
if not self.blockchain.is_connected():
|
|
1362
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1363
|
+
followers_list = []
|
|
1364
|
+
limit_reached = True
|
|
1365
|
+
cnt = 0
|
|
1366
|
+
while limit_reached:
|
|
1367
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1368
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1369
|
+
query = {"account": self.name, "start": last_user, "type": what, "limit": limit}
|
|
1370
|
+
if direction == "follower":
|
|
1371
|
+
try:
|
|
1372
|
+
followers = self.blockchain.rpc.get_followers(query, api="condenser")
|
|
1373
|
+
except:
|
|
1374
|
+
followers = self.blockchain.rpc.get_followers(
|
|
1375
|
+
self.name, last_user, what, limit, api="condenser"
|
|
1376
|
+
)
|
|
1377
|
+
if isinstance(followers, dict) and "followers" in followers:
|
|
1378
|
+
followers = followers["followers"]
|
|
1379
|
+
elif direction == "following":
|
|
1380
|
+
try:
|
|
1381
|
+
followers = self.blockchain.rpc.get_following(query, api="condenser")
|
|
1382
|
+
except:
|
|
1383
|
+
followers = self.blockchain.rpc.get_following(
|
|
1384
|
+
self.name, last_user, what, limit, api="condenser"
|
|
1385
|
+
)
|
|
1386
|
+
if isinstance(followers, dict) and "following" in followers:
|
|
1387
|
+
followers = followers["following"]
|
|
1388
|
+
else:
|
|
1389
|
+
if direction == "follower":
|
|
1390
|
+
try:
|
|
1391
|
+
followers = self.blockchain.rpc.get_followers(
|
|
1392
|
+
self.name, last_user, what, limit, api="condenser"
|
|
1393
|
+
)
|
|
1394
|
+
except:
|
|
1395
|
+
followers = self.blockchain.rpc.get_followers(
|
|
1396
|
+
[self.name, last_user, what, limit], api="condenser"
|
|
1397
|
+
)
|
|
1398
|
+
elif direction == "following":
|
|
1399
|
+
try:
|
|
1400
|
+
followers = self.blockchain.rpc.get_following(
|
|
1401
|
+
self.name, last_user, what, limit, api="condenser"
|
|
1402
|
+
)
|
|
1403
|
+
except:
|
|
1404
|
+
followers = self.blockchain.rpc.get_following(
|
|
1405
|
+
self.name, last_user, what, limit, api="condenser"
|
|
1406
|
+
)
|
|
1407
|
+
if cnt == 0:
|
|
1408
|
+
followers_list = followers
|
|
1409
|
+
elif followers is not None and len(followers) > 1:
|
|
1410
|
+
followers_list += followers[1:]
|
|
1411
|
+
if followers is not None and len(followers) >= limit:
|
|
1412
|
+
last_user = followers[-1][direction]
|
|
1413
|
+
limit_reached = True
|
|
1414
|
+
cnt += 1
|
|
1415
|
+
else:
|
|
1416
|
+
limit_reached = False
|
|
1417
|
+
|
|
1418
|
+
return followers_list
|
|
1419
|
+
|
|
1420
|
+
def list_all_subscriptions(self, account=None):
|
|
1421
|
+
"""Returns all subscriptions"""
|
|
1422
|
+
if account is None:
|
|
1423
|
+
account = self["name"]
|
|
1424
|
+
account = extract_account_name(account)
|
|
1425
|
+
if not self.blockchain.is_connected():
|
|
1426
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1427
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
1428
|
+
return self.blockchain.rpc.list_all_subscriptions({"account": account}, api="bridge")
|
|
1429
|
+
|
|
1430
|
+
def get_account_posts(self, sort="feed", limit=20, account=None, observer=None, raw_data=False):
|
|
1431
|
+
"""Returns account feed"""
|
|
1432
|
+
if account is None:
|
|
1433
|
+
account = self["name"]
|
|
1434
|
+
account = extract_account_name(account)
|
|
1435
|
+
if observer is None:
|
|
1436
|
+
observer = account
|
|
1437
|
+
if not self.blockchain.is_connected():
|
|
1438
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1439
|
+
from nectar.comment import AccountPosts
|
|
1440
|
+
|
|
1441
|
+
return AccountPosts(sort, account, observer=observer, limit=limit, raw_data=raw_data)
|
|
1442
|
+
|
|
1443
|
+
@property
|
|
1444
|
+
def available_balances(self):
|
|
1445
|
+
"""List balances of an account. This call returns instances of
|
|
1446
|
+
:class:`nectar.amount.Amount`.
|
|
1447
|
+
"""
|
|
1448
|
+
if "sbd_balance" in self:
|
|
1449
|
+
amount_list = ["balance", "sbd_balance", "vesting_shares"]
|
|
1450
|
+
elif "hbd_balance" in self:
|
|
1451
|
+
amount_list = ["balance", "hbd_balance", "vesting_shares"]
|
|
1452
|
+
else:
|
|
1453
|
+
amount_list = ["balance", "vesting_shares"]
|
|
1454
|
+
available_amount = []
|
|
1455
|
+
for amount in amount_list:
|
|
1456
|
+
if amount in self:
|
|
1457
|
+
available_amount.append(self[amount].copy())
|
|
1458
|
+
return available_amount
|
|
1459
|
+
|
|
1460
|
+
@property
|
|
1461
|
+
def saving_balances(self):
|
|
1462
|
+
savings_amount = []
|
|
1463
|
+
if "savings_sbd_balance" in self:
|
|
1464
|
+
amount_list = ["savings_balance", "savings_sbd_balance"]
|
|
1465
|
+
elif "savings_hbd_balance" in self:
|
|
1466
|
+
amount_list = ["savings_balance", "savings_hbd_balance"]
|
|
1467
|
+
else:
|
|
1468
|
+
amount_list = ["savings_balance"]
|
|
1469
|
+
for amount in amount_list:
|
|
1470
|
+
if amount in self:
|
|
1471
|
+
savings_amount.append(self[amount].copy())
|
|
1472
|
+
return savings_amount
|
|
1473
|
+
|
|
1474
|
+
@property
|
|
1475
|
+
def reward_balances(self):
|
|
1476
|
+
if "reward_steem_balance" in self and "reward_sbd_balance" in self:
|
|
1477
|
+
amount_list = ["reward_steem_balance", "reward_sbd_balance", "reward_vesting_balance"]
|
|
1478
|
+
elif "reward_hive_balance" in self and "reward_hbd_balance" in self:
|
|
1479
|
+
amount_list = ["reward_hive_balance", "reward_hbd_balance", "reward_vesting_balance"]
|
|
1480
|
+
else:
|
|
1481
|
+
amount_list = []
|
|
1482
|
+
rewards_amount = []
|
|
1483
|
+
for amount in amount_list:
|
|
1484
|
+
if amount in self:
|
|
1485
|
+
rewards_amount.append(self[amount].copy())
|
|
1486
|
+
return rewards_amount
|
|
1487
|
+
|
|
1488
|
+
@property
|
|
1489
|
+
def total_balances(self):
|
|
1490
|
+
symbols = []
|
|
1491
|
+
for balance in self.available_balances:
|
|
1492
|
+
symbols.append(balance["symbol"])
|
|
1493
|
+
ret = []
|
|
1494
|
+
for i in range(len(symbols)):
|
|
1495
|
+
balance_sum = self.get_balance(self.available_balances, symbols[i])
|
|
1496
|
+
balance_sum = balance_sum + self.get_balance(self.saving_balances, symbols[i])
|
|
1497
|
+
balance_sum = balance_sum + self.get_balance(self.reward_balances, symbols[i])
|
|
1498
|
+
ret.append(balance_sum)
|
|
1499
|
+
return ret
|
|
1500
|
+
|
|
1501
|
+
@property
|
|
1502
|
+
def balances(self):
|
|
1503
|
+
"""Returns all account balances as dictionary"""
|
|
1504
|
+
return self.get_balances()
|
|
1505
|
+
|
|
1506
|
+
def get_balances(self):
|
|
1507
|
+
"""Returns all account balances as dictionary
|
|
1508
|
+
|
|
1509
|
+
:returns: Account balances
|
|
1510
|
+
:rtype: dictionary
|
|
1511
|
+
|
|
1512
|
+
Sample output:
|
|
1513
|
+
|
|
1514
|
+
.. code-block:: js
|
|
1515
|
+
|
|
1516
|
+
{
|
|
1517
|
+
'available': [102.985 STEEM, 0.008 SBD, 146273.695970 VESTS],
|
|
1518
|
+
'savings': [0.000 STEEM, 0.000 SBD],
|
|
1519
|
+
'rewards': [0.000 STEEM, 0.000 SBD, 0.000000 VESTS],
|
|
1520
|
+
'total': [102.985 STEEM, 0.008 SBD, 146273.695970 VESTS]
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
"""
|
|
1524
|
+
return {
|
|
1525
|
+
"available": self.available_balances,
|
|
1526
|
+
"savings": self.saving_balances,
|
|
1527
|
+
"rewards": self.reward_balances,
|
|
1528
|
+
"total": self.total_balances,
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
def get_balance(self, balances, symbol):
|
|
1532
|
+
"""Obtain the balance of a specific Asset. This call returns instances of
|
|
1533
|
+
:class:`nectar.amount.Amount`. Available balance types:
|
|
1534
|
+
|
|
1535
|
+
* "available"
|
|
1536
|
+
* "saving"
|
|
1537
|
+
* "reward"
|
|
1538
|
+
* "total"
|
|
1539
|
+
|
|
1540
|
+
:param str balances: Defines the balance type
|
|
1541
|
+
:param symbol: Can be "SBD", "STEEM" or "VESTS
|
|
1542
|
+
:type symbol: str, dict
|
|
1543
|
+
|
|
1544
|
+
.. code-block:: python
|
|
1545
|
+
|
|
1546
|
+
>>> from nectar.account import Account
|
|
1547
|
+
>>> from nectar import Hive
|
|
1548
|
+
>>> from nectar.nodelist import NodeList
|
|
1549
|
+
>>> nodelist = NodeList()
|
|
1550
|
+
>>> nodelist.update_nodes()
|
|
1551
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1552
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
1553
|
+
>>> account.get_balance("rewards", "HBD")
|
|
1554
|
+
0.000 HBD
|
|
1555
|
+
|
|
1556
|
+
"""
|
|
1557
|
+
if isinstance(balances, string_types):
|
|
1558
|
+
if balances == "available":
|
|
1559
|
+
balances = self.available_balances
|
|
1560
|
+
elif balances == "savings":
|
|
1561
|
+
balances = self.saving_balances
|
|
1562
|
+
elif balances == "rewards":
|
|
1563
|
+
balances = self.reward_balances
|
|
1564
|
+
elif balances == "total":
|
|
1565
|
+
balances = self.total_balances
|
|
1566
|
+
else:
|
|
1567
|
+
return
|
|
1568
|
+
|
|
1569
|
+
if isinstance(symbol, dict) and "symbol" in symbol:
|
|
1570
|
+
symbol = symbol["symbol"]
|
|
1571
|
+
|
|
1572
|
+
for b in balances:
|
|
1573
|
+
if b["symbol"] == symbol:
|
|
1574
|
+
return b
|
|
1575
|
+
from .amount import Amount
|
|
1576
|
+
|
|
1577
|
+
return Amount(0, symbol, blockchain_instance=self.blockchain)
|
|
1578
|
+
|
|
1579
|
+
def interest(self):
|
|
1580
|
+
"""Calculate interest for an account
|
|
1581
|
+
|
|
1582
|
+
:param str account: Account name to get interest for
|
|
1583
|
+
:rtype: dictionary
|
|
1584
|
+
|
|
1585
|
+
Sample output:
|
|
1586
|
+
|
|
1587
|
+
.. code-block:: js
|
|
1588
|
+
|
|
1589
|
+
{
|
|
1590
|
+
'interest': 0.0,
|
|
1591
|
+
'last_payment': datetime.datetime(2018, 1, 26, 5, 50, 27, tzinfo=<UTC>),
|
|
1592
|
+
'next_payment': datetime.datetime(2018, 2, 25, 5, 50, 27, tzinfo=<UTC>),
|
|
1593
|
+
'next_payment_duration': datetime.timedelta(-65, 52132, 684026),
|
|
1594
|
+
'interest_rate': 0.0
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
"""
|
|
1598
|
+
interest_amount = 0
|
|
1599
|
+
interest_rate = 0
|
|
1600
|
+
next_payment = datetime(1970, 1, 1, 0, 0, 0)
|
|
1601
|
+
last_payment = datetime(1970, 1, 1, 0, 0, 0)
|
|
1602
|
+
if "sbd_last_interest_payment" in self:
|
|
1603
|
+
last_payment = self["sbd_last_interest_payment"]
|
|
1604
|
+
next_payment = last_payment + timedelta(days=30)
|
|
1605
|
+
interest_rate = (
|
|
1606
|
+
self.blockchain.get_dynamic_global_properties()["sbd_interest_rate"] / 100
|
|
1607
|
+
) # percent
|
|
1608
|
+
interest_amount = (
|
|
1609
|
+
(interest_rate / 100)
|
|
1610
|
+
* int(int(self["sbd_seconds"]) / (60 * 60 * 24 * 356))
|
|
1611
|
+
* 10**-3
|
|
1612
|
+
)
|
|
1613
|
+
elif "hbd_last_interest_payment" in self:
|
|
1614
|
+
last_payment = self["hbd_last_interest_payment"]
|
|
1615
|
+
next_payment = last_payment + timedelta(days=30)
|
|
1616
|
+
interest_rate = (
|
|
1617
|
+
self.blockchain.get_dynamic_global_properties()["hbd_interest_rate"] / 100
|
|
1618
|
+
) # percent
|
|
1619
|
+
interest_amount = (
|
|
1620
|
+
(interest_rate / 100)
|
|
1621
|
+
* int(int(self["hbd_seconds"]) / (60 * 60 * 24 * 356))
|
|
1622
|
+
* 10**-3
|
|
1623
|
+
)
|
|
1624
|
+
return {
|
|
1625
|
+
"interest": interest_amount,
|
|
1626
|
+
"last_payment": last_payment,
|
|
1627
|
+
"next_payment": next_payment,
|
|
1628
|
+
"next_payment_duration": next_payment - addTzInfo(datetime.now(timezone.utc)),
|
|
1629
|
+
"interest_rate": interest_rate,
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
@property
|
|
1633
|
+
def is_fully_loaded(self):
|
|
1634
|
+
"""Is this instance fully loaded / e.g. all data available?
|
|
1635
|
+
|
|
1636
|
+
:rtype: bool
|
|
1637
|
+
"""
|
|
1638
|
+
return self.full
|
|
1639
|
+
|
|
1640
|
+
def ensure_full(self):
|
|
1641
|
+
"""Ensure that all data are loaded"""
|
|
1642
|
+
if not self.is_fully_loaded:
|
|
1643
|
+
self.full = True
|
|
1644
|
+
self.refresh()
|
|
1645
|
+
|
|
1646
|
+
def get_account_bandwidth(self, bandwidth_type=1, account=None):
|
|
1647
|
+
"""get_account_bandwidth"""
|
|
1648
|
+
if account is None:
|
|
1649
|
+
account = self["name"]
|
|
1650
|
+
account = extract_account_name(account)
|
|
1651
|
+
if not self.blockchain.is_connected():
|
|
1652
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1653
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1654
|
+
return self.blockchain.rpc.get_account_bandwidth(account, bandwidth_type)
|
|
1655
|
+
|
|
1656
|
+
def get_bandwidth(self):
|
|
1657
|
+
"""Returns used and allocated bandwidth
|
|
1658
|
+
|
|
1659
|
+
:rtype: dictionary
|
|
1660
|
+
|
|
1661
|
+
Sample output:
|
|
1662
|
+
|
|
1663
|
+
.. code-block:: js
|
|
1664
|
+
|
|
1665
|
+
{
|
|
1666
|
+
'used': 0,
|
|
1667
|
+
'allocated': 2211037
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
"""
|
|
1671
|
+
account = self["name"]
|
|
1672
|
+
global_properties = self.blockchain.get_dynamic_global_properties()
|
|
1673
|
+
try:
|
|
1674
|
+
reserve_ratio = self.blockchain.get_reserve_ratio()
|
|
1675
|
+
except:
|
|
1676
|
+
return {"used": 0, "allocated": 0}
|
|
1677
|
+
if "received_vesting_shares" in self:
|
|
1678
|
+
received_vesting_shares = self["received_vesting_shares"].amount
|
|
1679
|
+
else:
|
|
1680
|
+
received_vesting_shares = 0
|
|
1681
|
+
vesting_shares = self["vesting_shares"].amount
|
|
1682
|
+
if reserve_ratio is None or reserve_ratio["max_virtual_bandwidth"] is None:
|
|
1683
|
+
return {"used": None, "allocated": None}
|
|
1684
|
+
max_virtual_bandwidth = float(reserve_ratio["max_virtual_bandwidth"])
|
|
1685
|
+
total_vesting_shares = Amount(
|
|
1686
|
+
global_properties["total_vesting_shares"], blockchain_instance=self.blockchain
|
|
1687
|
+
).amount
|
|
1688
|
+
allocated_bandwidth = (
|
|
1689
|
+
max_virtual_bandwidth
|
|
1690
|
+
* (vesting_shares + received_vesting_shares)
|
|
1691
|
+
/ total_vesting_shares
|
|
1692
|
+
)
|
|
1693
|
+
allocated_bandwidth = round(allocated_bandwidth / 1000000)
|
|
1694
|
+
|
|
1695
|
+
if self.blockchain.is_connected() and self.blockchain.rpc.get_use_appbase():
|
|
1696
|
+
try:
|
|
1697
|
+
account_bandwidth = self.get_account_bandwidth(bandwidth_type=1, account=account)
|
|
1698
|
+
except:
|
|
1699
|
+
account_bandwidth = None
|
|
1700
|
+
if account_bandwidth is None:
|
|
1701
|
+
return {"used": 0, "allocated": allocated_bandwidth}
|
|
1702
|
+
last_bandwidth_update = formatTimeString(account_bandwidth["last_bandwidth_update"])
|
|
1703
|
+
average_bandwidth = float(account_bandwidth["average_bandwidth"])
|
|
1704
|
+
else:
|
|
1705
|
+
last_bandwidth_update = self["last_bandwidth_update"]
|
|
1706
|
+
average_bandwidth = float(self["average_bandwidth"])
|
|
1707
|
+
total_seconds = 604800
|
|
1708
|
+
|
|
1709
|
+
seconds_since_last_update = addTzInfo(datetime.now(timezone.utc)) - last_bandwidth_update
|
|
1710
|
+
seconds_since_last_update = seconds_since_last_update.total_seconds()
|
|
1711
|
+
used_bandwidth = 0
|
|
1712
|
+
if seconds_since_last_update < total_seconds:
|
|
1713
|
+
used_bandwidth = (
|
|
1714
|
+
(total_seconds - seconds_since_last_update) * average_bandwidth
|
|
1715
|
+
) / total_seconds
|
|
1716
|
+
used_bandwidth = round(used_bandwidth / 1000000)
|
|
1717
|
+
|
|
1718
|
+
return {"used": used_bandwidth, "allocated": allocated_bandwidth}
|
|
1719
|
+
# print("bandwidth percent used: " + str(100 * used_bandwidth / allocated_bandwidth))
|
|
1720
|
+
# print("bandwidth percent remaining: " + str(100 - (100 * used_bandwidth / allocated_bandwidth)))
|
|
1721
|
+
|
|
1722
|
+
def get_owner_history(self, account=None):
|
|
1723
|
+
"""Returns the owner history of an account.
|
|
1724
|
+
|
|
1725
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
1726
|
+
|
|
1727
|
+
:rtype: list
|
|
1728
|
+
|
|
1729
|
+
.. code-block:: python
|
|
1730
|
+
|
|
1731
|
+
>>> from nectar.account import Account
|
|
1732
|
+
>>> from nectar import Hive
|
|
1733
|
+
>>> from nectar.nodelist import NodeList
|
|
1734
|
+
>>> nodelist = NodeList()
|
|
1735
|
+
>>> nodelist.update_nodes()
|
|
1736
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1737
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
1738
|
+
>>> account.get_owner_history()
|
|
1739
|
+
[]
|
|
1740
|
+
|
|
1741
|
+
"""
|
|
1742
|
+
if account is None:
|
|
1743
|
+
account = self["name"]
|
|
1744
|
+
account = extract_account_name(account)
|
|
1745
|
+
if not self.blockchain.is_connected():
|
|
1746
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1747
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1748
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1749
|
+
return self.blockchain.rpc.find_owner_histories({"owner": account}, api="database")[
|
|
1750
|
+
"owner_auths"
|
|
1751
|
+
]
|
|
1752
|
+
else:
|
|
1753
|
+
return self.blockchain.rpc.get_owner_history(account)
|
|
1754
|
+
|
|
1755
|
+
def get_conversion_requests(self, account=None):
|
|
1756
|
+
"""Returns a list of SBD conversion request
|
|
1757
|
+
|
|
1758
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
1759
|
+
|
|
1760
|
+
:rtype: list
|
|
1761
|
+
|
|
1762
|
+
.. code-block:: python
|
|
1763
|
+
|
|
1764
|
+
>>> from nectar.account import Account
|
|
1765
|
+
>>> from nectar import Hive
|
|
1766
|
+
>>> from nectar.nodelist import NodeList
|
|
1767
|
+
>>> nodelist = NodeList()
|
|
1768
|
+
>>> nodelist.update_nodes()
|
|
1769
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1770
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
1771
|
+
>>> account.get_conversion_requests()
|
|
1772
|
+
[]
|
|
1773
|
+
|
|
1774
|
+
"""
|
|
1775
|
+
if account is None:
|
|
1776
|
+
account = self["name"]
|
|
1777
|
+
account = extract_account_name(account)
|
|
1778
|
+
if not self.blockchain.is_connected():
|
|
1779
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1780
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1781
|
+
if self.blockchain.rpc.get_use_appbase() and "sbd_balance" in self:
|
|
1782
|
+
return self.blockchain.rpc.find_sbd_conversion_requests(
|
|
1783
|
+
{"account": account}, api="database"
|
|
1784
|
+
)["requests"]
|
|
1785
|
+
elif self.blockchain.rpc.get_use_appbase() and "hbd_balance" in self:
|
|
1786
|
+
return self.blockchain.rpc.find_hbd_conversion_requests(
|
|
1787
|
+
{"account": account}, api="database"
|
|
1788
|
+
)["requests"]
|
|
1789
|
+
else:
|
|
1790
|
+
return self.blockchain.rpc.get_conversion_requests(account)
|
|
1791
|
+
|
|
1792
|
+
def get_vesting_delegations(self, start_account="", limit=100, account=None):
|
|
1793
|
+
"""Returns the vesting delegations by an account.
|
|
1794
|
+
|
|
1795
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
1796
|
+
:param str start_account: delegatee to start with, leave empty to start from the first by name
|
|
1797
|
+
:param int limit: maximum number of results to return
|
|
1798
|
+
:rtype: list
|
|
1799
|
+
|
|
1800
|
+
.. code-block:: python
|
|
1801
|
+
|
|
1802
|
+
>>> from nectar.account import Account
|
|
1803
|
+
>>> from nectar import Hive
|
|
1804
|
+
>>> from nectar.nodelist import NodeList
|
|
1805
|
+
>>> nodelist = NodeList()
|
|
1806
|
+
>>> nodelist.update_nodes()
|
|
1807
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1808
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
1809
|
+
>>> account.get_vesting_delegations()
|
|
1810
|
+
[]
|
|
1811
|
+
|
|
1812
|
+
"""
|
|
1813
|
+
if account is None:
|
|
1814
|
+
account = self["name"]
|
|
1815
|
+
account = extract_account_name(account)
|
|
1816
|
+
if not self.blockchain.is_connected():
|
|
1817
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1818
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1819
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1820
|
+
delegations = self.blockchain.rpc.list_vesting_delegations(
|
|
1821
|
+
{"start": [account, start_account], "limit": limit, "order": "by_delegation"},
|
|
1822
|
+
api="database",
|
|
1823
|
+
)["delegations"]
|
|
1824
|
+
return [d for d in delegations if d["delegator"] == account]
|
|
1825
|
+
else:
|
|
1826
|
+
return self.blockchain.rpc.get_vesting_delegations(account, start_account, limit)
|
|
1827
|
+
|
|
1828
|
+
def get_withdraw_routes(self, account=None):
|
|
1829
|
+
"""Returns the withdraw routes for an account.
|
|
1830
|
+
|
|
1831
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
1832
|
+
|
|
1833
|
+
:rtype: list
|
|
1834
|
+
|
|
1835
|
+
.. code-block:: python
|
|
1836
|
+
|
|
1837
|
+
>>> from nectar.account import Account
|
|
1838
|
+
>>> from nectar import Hive
|
|
1839
|
+
>>> from nectar.nodelist import NodeList
|
|
1840
|
+
>>> nodelist = NodeList()
|
|
1841
|
+
>>> nodelist.update_nodes()
|
|
1842
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1843
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
1844
|
+
>>> account.get_withdraw_routes()
|
|
1845
|
+
[]
|
|
1846
|
+
|
|
1847
|
+
"""
|
|
1848
|
+
if account is None:
|
|
1849
|
+
account = self["name"]
|
|
1850
|
+
account = extract_account_name(account)
|
|
1851
|
+
if not self.blockchain.is_connected():
|
|
1852
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1853
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1854
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1855
|
+
return self.blockchain.rpc.find_withdraw_vesting_routes(
|
|
1856
|
+
{"account": account, "order": "by_withdraw_route"}, api="database"
|
|
1857
|
+
)["routes"]
|
|
1858
|
+
else:
|
|
1859
|
+
return self.blockchain.rpc.get_withdraw_routes(account, "all")
|
|
1860
|
+
|
|
1861
|
+
def get_savings_withdrawals(self, direction="from", account=None):
|
|
1862
|
+
"""Returns the list of savings withdrawls for an account.
|
|
1863
|
+
|
|
1864
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
1865
|
+
:param str direction: Can be either from or to (only non appbase nodes)
|
|
1866
|
+
|
|
1867
|
+
:rtype: list
|
|
1868
|
+
|
|
1869
|
+
.. code-block:: python
|
|
1870
|
+
|
|
1871
|
+
>>> from nectar.account import Account
|
|
1872
|
+
>>> from nectar import Hive
|
|
1873
|
+
>>> from nectar.nodelist import NodeList
|
|
1874
|
+
>>> nodelist = NodeList()
|
|
1875
|
+
>>> nodelist.update_nodes()
|
|
1876
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1877
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
1878
|
+
>>> account.get_savings_withdrawals()
|
|
1879
|
+
[]
|
|
1880
|
+
|
|
1881
|
+
"""
|
|
1882
|
+
if account is None:
|
|
1883
|
+
account = self["name"]
|
|
1884
|
+
account = extract_account_name(account)
|
|
1885
|
+
if not self.blockchain.is_connected():
|
|
1886
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1887
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1888
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1889
|
+
return self.blockchain.rpc.find_savings_withdrawals(
|
|
1890
|
+
{"account": account}, api="database"
|
|
1891
|
+
)["withdrawals"]
|
|
1892
|
+
elif direction == "from":
|
|
1893
|
+
return self.blockchain.rpc.get_savings_withdraw_from(account)
|
|
1894
|
+
elif direction == "to":
|
|
1895
|
+
return self.blockchain.rpc.get_savings_withdraw_to(account)
|
|
1896
|
+
|
|
1897
|
+
def get_recovery_request(self, account=None):
|
|
1898
|
+
"""Returns the recovery request for an account
|
|
1899
|
+
|
|
1900
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
1901
|
+
|
|
1902
|
+
:rtype: list
|
|
1903
|
+
|
|
1904
|
+
.. code-block:: python
|
|
1905
|
+
|
|
1906
|
+
>>> from nectar.account import Account
|
|
1907
|
+
>>> from nectar import Hive
|
|
1908
|
+
>>> from nectar.nodelist import NodeList
|
|
1909
|
+
>>> nodelist = NodeList()
|
|
1910
|
+
>>> nodelist.update_nodes()
|
|
1911
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1912
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
1913
|
+
>>> account.get_recovery_request()
|
|
1914
|
+
[]
|
|
1915
|
+
|
|
1916
|
+
"""
|
|
1917
|
+
if account is None:
|
|
1918
|
+
account = self["name"]
|
|
1919
|
+
account = extract_account_name(account)
|
|
1920
|
+
if not self.blockchain.is_connected():
|
|
1921
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1922
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1923
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1924
|
+
return self.blockchain.rpc.find_account_recovery_requests(
|
|
1925
|
+
{"account": account}, api="database"
|
|
1926
|
+
)["requests"]
|
|
1927
|
+
else:
|
|
1928
|
+
return self.blockchain.rpc.get_recovery_request(account)
|
|
1929
|
+
|
|
1930
|
+
def get_escrow(self, escrow_id=0, account=None):
|
|
1931
|
+
"""Returns the escrow for a certain account by id
|
|
1932
|
+
|
|
1933
|
+
:param int escrow_id: Id (only pre appbase)
|
|
1934
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
1935
|
+
|
|
1936
|
+
:rtype: list
|
|
1937
|
+
|
|
1938
|
+
.. code-block:: python
|
|
1939
|
+
|
|
1940
|
+
>>> from nectar.account import Account
|
|
1941
|
+
>>> from nectar import Hive
|
|
1942
|
+
>>> from nectar.nodelist import NodeList
|
|
1943
|
+
>>> nodelist = NodeList()
|
|
1944
|
+
>>> nodelist.update_nodes()
|
|
1945
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1946
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
1947
|
+
>>> account.get_escrow(1234)
|
|
1948
|
+
[]
|
|
1949
|
+
|
|
1950
|
+
"""
|
|
1951
|
+
if account is None:
|
|
1952
|
+
account = self["name"]
|
|
1953
|
+
account = extract_account_name(account)
|
|
1954
|
+
if not self.blockchain.is_connected():
|
|
1955
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1956
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1957
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1958
|
+
return self.blockchain.rpc.find_escrows({"from": account}, api="database")["escrows"]
|
|
1959
|
+
else:
|
|
1960
|
+
return self.blockchain.rpc.get_escrow(account, escrow_id)
|
|
1961
|
+
|
|
1962
|
+
def verify_account_authority(self, keys, account=None):
|
|
1963
|
+
"""Returns true if the signers have enough authority to authorize an account.
|
|
1964
|
+
|
|
1965
|
+
:param list keys: public key
|
|
1966
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
1967
|
+
|
|
1968
|
+
:rtype: dictionary
|
|
1969
|
+
|
|
1970
|
+
.. code-block:: python
|
|
1971
|
+
|
|
1972
|
+
>>> from nectar.account import Account
|
|
1973
|
+
>>> from nectar import Hive
|
|
1974
|
+
>>> from nectar.nodelist import NodeList
|
|
1975
|
+
>>> nodelist = NodeList()
|
|
1976
|
+
>>> nodelist.update_nodes()
|
|
1977
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
1978
|
+
>>> account = Account("steemit", blockchain_instance=stm)
|
|
1979
|
+
>>> print(account.verify_account_authority(["STM7Q2rLBqzPzFeteQZewv9Lu3NLE69fZoLeL6YK59t7UmssCBNTU"])["valid"])
|
|
1980
|
+
False
|
|
1981
|
+
|
|
1982
|
+
"""
|
|
1983
|
+
if account is None:
|
|
1984
|
+
account = self["name"]
|
|
1985
|
+
account = extract_account_name(account)
|
|
1986
|
+
if not self.blockchain.is_connected():
|
|
1987
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
1988
|
+
if not isinstance(keys, list):
|
|
1989
|
+
keys = [keys]
|
|
1990
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
1991
|
+
try:
|
|
1992
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
1993
|
+
return self.blockchain.rpc.verify_account_authority(
|
|
1994
|
+
{"account": account, "signers": keys}, api="database"
|
|
1995
|
+
)
|
|
1996
|
+
else:
|
|
1997
|
+
return self.blockchain.rpc.verify_account_authority(account, keys)
|
|
1998
|
+
except MissingRequiredActiveAuthority:
|
|
1999
|
+
return {"valid": False}
|
|
2000
|
+
|
|
2001
|
+
def get_tags_used_by_author(self, account=None):
|
|
2002
|
+
"""Returns a list of tags used by an author.
|
|
2003
|
+
|
|
2004
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
2005
|
+
|
|
2006
|
+
:rtype: list
|
|
2007
|
+
|
|
2008
|
+
"""
|
|
2009
|
+
if account is None:
|
|
2010
|
+
account = self["name"]
|
|
2011
|
+
account = extract_account_name(account)
|
|
2012
|
+
if not self.blockchain.is_connected():
|
|
2013
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
2014
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
2015
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
2016
|
+
return self.blockchain.rpc.get_tags_used_by_author(
|
|
2017
|
+
{"author": account}, api="condenser"
|
|
2018
|
+
)["tags"]
|
|
2019
|
+
else:
|
|
2020
|
+
return self.blockchain.rpc.get_tags_used_by_author(account, api="condenser")
|
|
2021
|
+
|
|
2022
|
+
def get_expiring_vesting_delegations(self, after=None, limit=1000, account=None):
|
|
2023
|
+
"""Returns the expirations for vesting delegations.
|
|
2024
|
+
|
|
2025
|
+
:param datetime after: expiration after (only for pre appbase nodes)
|
|
2026
|
+
:param int limit: limits number of shown entries (only for pre appbase nodes)
|
|
2027
|
+
:param str account: When set, a different account is used for the request (Default is object account name)
|
|
2028
|
+
|
|
2029
|
+
:rtype: list
|
|
2030
|
+
|
|
2031
|
+
.. code-block:: python
|
|
2032
|
+
|
|
2033
|
+
>>> from nectar.account import Account
|
|
2034
|
+
>>> from nectar import Hive
|
|
2035
|
+
>>> from nectar.nodelist import NodeList
|
|
2036
|
+
>>> nodelist = NodeList()
|
|
2037
|
+
>>> nodelist.update_nodes()
|
|
2038
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
2039
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
2040
|
+
>>> account.get_expiring_vesting_delegations()
|
|
2041
|
+
[]
|
|
2042
|
+
|
|
2043
|
+
"""
|
|
2044
|
+
if account is None:
|
|
2045
|
+
account = self["name"]
|
|
2046
|
+
account = extract_account_name(account)
|
|
2047
|
+
if not self.blockchain.is_connected():
|
|
2048
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
2049
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
2050
|
+
if after is None:
|
|
2051
|
+
after = addTzInfo(datetime.now(timezone.utc)) - timedelta(days=8)
|
|
2052
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
2053
|
+
return self.blockchain.rpc.find_vesting_delegation_expirations(
|
|
2054
|
+
{"account": account}, api="database"
|
|
2055
|
+
)["delegations"]
|
|
2056
|
+
else:
|
|
2057
|
+
return self.blockchain.rpc.get_expiring_vesting_delegations(
|
|
2058
|
+
account, formatTimeString(after), limit
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
def get_account_votes(
|
|
2062
|
+
self, account=None, start_author="", start_permlink="", limit=1000, start_date=None
|
|
2063
|
+
):
|
|
2064
|
+
"""Returns all votes that the account has done
|
|
2065
|
+
|
|
2066
|
+
:rtype: list
|
|
2067
|
+
|
|
2068
|
+
.. code-block:: python
|
|
2069
|
+
|
|
2070
|
+
>>> from nectar.account import Account
|
|
2071
|
+
>>> from nectar import Hive
|
|
2072
|
+
>>> from nectar.nodelist import NodeList
|
|
2073
|
+
>>> nodelist = NodeList()
|
|
2074
|
+
>>> nodelist.update_nodes()
|
|
2075
|
+
>>> stm = Hive(node=nodelist.get_hive_nodes())
|
|
2076
|
+
>>> account = Account("nectar.app", blockchain_instance=stm)
|
|
2077
|
+
>>> account.get_account_votes() # doctest: +SKIP
|
|
2078
|
+
|
|
2079
|
+
"""
|
|
2080
|
+
if account is None:
|
|
2081
|
+
account = self["name"]
|
|
2082
|
+
account = extract_account_name(account)
|
|
2083
|
+
if not self.blockchain.is_connected():
|
|
2084
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
2085
|
+
# self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
2086
|
+
# if self.blockchain.rpc.get_use_appbase():
|
|
2087
|
+
# vote_list = self.blockchain.rpc.get_account_votes(account, api="condenser")
|
|
2088
|
+
# else:
|
|
2089
|
+
# vote_list = self.blockchain.rpc.get_account_votes(account)
|
|
2090
|
+
# if isinstance(vote_list, dict) and "error" in vote_list:
|
|
2091
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
2092
|
+
vote_list = []
|
|
2093
|
+
finished = False
|
|
2094
|
+
while not finished:
|
|
2095
|
+
try:
|
|
2096
|
+
ret = self.blockchain.rpc.list_votes(
|
|
2097
|
+
{
|
|
2098
|
+
"start": [account, start_author, start_permlink],
|
|
2099
|
+
"limit": limit,
|
|
2100
|
+
"order": "by_voter_comment",
|
|
2101
|
+
},
|
|
2102
|
+
api="database",
|
|
2103
|
+
)["votes"]
|
|
2104
|
+
except SupportedByHivemind:
|
|
2105
|
+
return vote_list
|
|
2106
|
+
if len(ret) < limit:
|
|
2107
|
+
finished = True
|
|
2108
|
+
if start_author != "":
|
|
2109
|
+
if len(ret) == 0:
|
|
2110
|
+
finished = True
|
|
2111
|
+
ret = ret[1:]
|
|
2112
|
+
for vote in ret:
|
|
2113
|
+
if vote["voter"] != account:
|
|
2114
|
+
finished = True
|
|
2115
|
+
continue
|
|
2116
|
+
last_update = formatTimeString(vote["last_update"])
|
|
2117
|
+
if start_date is not None and last_update < start_date:
|
|
2118
|
+
finished = True
|
|
2119
|
+
continue
|
|
2120
|
+
vote_list.append(vote)
|
|
2121
|
+
start_author = vote["author"]
|
|
2122
|
+
start_permlink = vote["permlink"]
|
|
2123
|
+
return vote_list
|
|
2124
|
+
# else:
|
|
2125
|
+
# return vote_list
|
|
2126
|
+
|
|
2127
|
+
def get_vote(self, comment):
|
|
2128
|
+
"""Returns a vote if the account has already voted for comment.
|
|
2129
|
+
|
|
2130
|
+
:param comment: can be a Comment object or a authorpermlink
|
|
2131
|
+
:type comment: str, Comment
|
|
2132
|
+
"""
|
|
2133
|
+
from nectar.comment import Comment
|
|
2134
|
+
|
|
2135
|
+
c = Comment(comment, blockchain_instance=self.blockchain)
|
|
2136
|
+
for v in c["active_votes"]:
|
|
2137
|
+
if v["voter"] == self["name"]:
|
|
2138
|
+
return v
|
|
2139
|
+
return None
|
|
2140
|
+
|
|
2141
|
+
def has_voted(self, comment):
|
|
2142
|
+
"""Returns if the account has already voted for comment
|
|
2143
|
+
|
|
2144
|
+
:param comment: can be a Comment object or a authorpermlink
|
|
2145
|
+
:type comment: str, Comment
|
|
2146
|
+
"""
|
|
2147
|
+
from nectar.comment import Comment
|
|
2148
|
+
|
|
2149
|
+
c = Comment(comment, blockchain_instance=self.blockchain)
|
|
2150
|
+
active_votes = {v["voter"]: v for v in c["active_votes"]}
|
|
2151
|
+
return self["name"] in active_votes
|
|
2152
|
+
|
|
2153
|
+
def virtual_op_count(self, until=None):
|
|
2154
|
+
"""Returns the number of individual account transactions
|
|
2155
|
+
|
|
2156
|
+
:rtype: list
|
|
2157
|
+
"""
|
|
2158
|
+
if until is not None:
|
|
2159
|
+
return self.estimate_virtual_op_num(until, stop_diff=1)
|
|
2160
|
+
else:
|
|
2161
|
+
try:
|
|
2162
|
+
op_count = 0
|
|
2163
|
+
op_count = self._get_account_history(start=-1, limit=1)
|
|
2164
|
+
if op_count is None or len(op_count) == 0:
|
|
2165
|
+
op_count = self._get_account_history(start=-1, limit=1)
|
|
2166
|
+
if isinstance(op_count, list) and len(op_count) > 0 and len(op_count[0]) > 0:
|
|
2167
|
+
if self.blockchain.rpc.url == "https://api.hive.blog":
|
|
2168
|
+
return op_count[-1][0] + 1
|
|
2169
|
+
return op_count[-1][0]
|
|
2170
|
+
else:
|
|
2171
|
+
return 0
|
|
2172
|
+
except IndexError:
|
|
2173
|
+
return 0
|
|
2174
|
+
|
|
2175
|
+
def _get_account_history(
|
|
2176
|
+
self, account=None, start=-1, limit=1, operation_filter_low=None, operation_filter_high=None
|
|
2177
|
+
):
|
|
2178
|
+
if account is None:
|
|
2179
|
+
account = self["name"]
|
|
2180
|
+
account = extract_account_name(account)
|
|
2181
|
+
if limit < 1:
|
|
2182
|
+
limit = 1
|
|
2183
|
+
elif limit > 1000:
|
|
2184
|
+
limit = 1000
|
|
2185
|
+
if not self.blockchain.is_connected():
|
|
2186
|
+
raise OfflineHasNoRPCException("No RPC available in offline mode!")
|
|
2187
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
2188
|
+
if operation_filter_low is None and operation_filter_high is None:
|
|
2189
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
2190
|
+
try:
|
|
2191
|
+
ret = self.blockchain.rpc.get_account_history(
|
|
2192
|
+
{"account": account, "start": start, "limit": limit}, api="account_history"
|
|
2193
|
+
)
|
|
2194
|
+
if ret is not None:
|
|
2195
|
+
ret = ret["history"]
|
|
2196
|
+
except ApiNotSupported:
|
|
2197
|
+
ret = self.blockchain.rpc.get_account_history(
|
|
2198
|
+
account, start, limit, api="condenser"
|
|
2199
|
+
)
|
|
2200
|
+
else:
|
|
2201
|
+
ret = self.blockchain.rpc.get_account_history(account, start, limit, api="database")
|
|
2202
|
+
else:
|
|
2203
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
2204
|
+
try:
|
|
2205
|
+
ret = self.blockchain.rpc.get_account_history(
|
|
2206
|
+
{
|
|
2207
|
+
"account": account,
|
|
2208
|
+
"start": start,
|
|
2209
|
+
"limit": limit,
|
|
2210
|
+
"operation_filter_low": operation_filter_low,
|
|
2211
|
+
"operation_filter_high": operation_filter_high,
|
|
2212
|
+
},
|
|
2213
|
+
api="account_history",
|
|
2214
|
+
)
|
|
2215
|
+
if ret is not None:
|
|
2216
|
+
ret = ret["history"]
|
|
2217
|
+
except ApiNotSupported:
|
|
2218
|
+
ret = self.blockchain.rpc.get_account_history(
|
|
2219
|
+
account,
|
|
2220
|
+
start,
|
|
2221
|
+
limit,
|
|
2222
|
+
operation_filter_low,
|
|
2223
|
+
operation_filter_high,
|
|
2224
|
+
api="condenser",
|
|
2225
|
+
)
|
|
2226
|
+
else:
|
|
2227
|
+
ret = self.blockchain.rpc.get_account_history(
|
|
2228
|
+
account,
|
|
2229
|
+
start,
|
|
2230
|
+
limit,
|
|
2231
|
+
operation_filter_low,
|
|
2232
|
+
operation_filter_high,
|
|
2233
|
+
api="database",
|
|
2234
|
+
)
|
|
2235
|
+
return ret
|
|
2236
|
+
|
|
2237
|
+
def _get_blocknum_from_hist(self, index, min_index=1):
|
|
2238
|
+
if index >= 0 and index < min_index:
|
|
2239
|
+
index = min_index
|
|
2240
|
+
op = self._get_account_history(start=(index))
|
|
2241
|
+
if len(op) == 0:
|
|
2242
|
+
return None
|
|
2243
|
+
return op[0][1]["block"]
|
|
2244
|
+
|
|
2245
|
+
def _get_first_blocknum(self):
|
|
2246
|
+
min_index = 0
|
|
2247
|
+
try:
|
|
2248
|
+
created = self._get_blocknum_from_hist(0, min_index=min_index)
|
|
2249
|
+
except:
|
|
2250
|
+
min_index = 1
|
|
2251
|
+
created = self._get_blocknum_from_hist(0, min_index=min_index)
|
|
2252
|
+
return created, min_index
|
|
2253
|
+
|
|
2254
|
+
def estimate_virtual_op_num(self, blocktime, stop_diff=0, max_count=100, min_index=None):
|
|
2255
|
+
"""Returns an estimation of an virtual operation index for a given time or blockindex
|
|
2256
|
+
|
|
2257
|
+
:param blocktime: start time or start block index from which account
|
|
2258
|
+
operation should be fetched
|
|
2259
|
+
:type blocktime: int, datetime
|
|
2260
|
+
:param int stop_diff: Sets the difference between last estimation and
|
|
2261
|
+
new estimation at which the estimation stops. Must not be zero. (default is 1)
|
|
2262
|
+
:param int max_count: sets the maximum number of iterations. -1 disables this (default 100)
|
|
2263
|
+
|
|
2264
|
+
.. testsetup::
|
|
2265
|
+
|
|
2266
|
+
from nectar.account import Account
|
|
2267
|
+
from nectar.blockchain import Blockchain
|
|
2268
|
+
from datetime import datetime, timedelta
|
|
2269
|
+
from timeit import time as t
|
|
2270
|
+
.. testcode::
|
|
2271
|
+
|
|
2272
|
+
start_time = datetime.now() - timedelta(days=7)
|
|
2273
|
+
acc = Account("gtg")
|
|
2274
|
+
start_op = acc.estimate_virtual_op_num(start_time)
|
|
2275
|
+
|
|
2276
|
+
b = Blockchain()
|
|
2277
|
+
start_block_num = b.get_estimated_block_num(start_time)
|
|
2278
|
+
start_op2 = acc.estimate_virtual_op_num(start_block_num)
|
|
2279
|
+
|
|
2280
|
+
.. testcode::
|
|
2281
|
+
|
|
2282
|
+
acc = Account("gtg")
|
|
2283
|
+
block_num = 21248120
|
|
2284
|
+
start = t.time()
|
|
2285
|
+
op_num = acc.estimate_virtual_op_num(block_num, stop_diff=1, max_count=10)
|
|
2286
|
+
stop = t.time()
|
|
2287
|
+
print(stop - start)
|
|
2288
|
+
for h in acc.get_account_history(op_num, 0):
|
|
2289
|
+
block_est = h["block"]
|
|
2290
|
+
print(block_est - block_num)
|
|
2291
|
+
|
|
2292
|
+
"""
|
|
2293
|
+
max_index = self.virtual_op_count()
|
|
2294
|
+
if max_index < stop_diff:
|
|
2295
|
+
return 0
|
|
2296
|
+
|
|
2297
|
+
# calculate everything with block numbers
|
|
2298
|
+
if min_index is None:
|
|
2299
|
+
created, min_index = self._get_first_blocknum()
|
|
2300
|
+
else:
|
|
2301
|
+
created = self._get_blocknum_from_hist(0, min_index=min_index)
|
|
2302
|
+
|
|
2303
|
+
# convert blocktime to block number if given as a datetime/date/time
|
|
2304
|
+
if isinstance(blocktime, (datetime, date, time)):
|
|
2305
|
+
b = Blockchain(blockchain_instance=self.blockchain)
|
|
2306
|
+
target_blocknum = b.get_estimated_block_num(addTzInfo(blocktime), accurate=True)
|
|
2307
|
+
else:
|
|
2308
|
+
target_blocknum = blocktime
|
|
2309
|
+
|
|
2310
|
+
# the requested blocknum/timestamp is before the account creation date
|
|
2311
|
+
if target_blocknum <= created:
|
|
2312
|
+
return 0
|
|
2313
|
+
|
|
2314
|
+
# get the block number from the account's latest operation
|
|
2315
|
+
latest_blocknum = self._get_blocknum_from_hist(-1, min_index=min_index)
|
|
2316
|
+
|
|
2317
|
+
# requested blocknum/timestamp is after the latest account operation
|
|
2318
|
+
if target_blocknum >= latest_blocknum:
|
|
2319
|
+
return max_index
|
|
2320
|
+
|
|
2321
|
+
# all account ops in a single block
|
|
2322
|
+
if latest_blocknum - created == 0:
|
|
2323
|
+
return 0
|
|
2324
|
+
|
|
2325
|
+
# set initial search range
|
|
2326
|
+
op_num = 0
|
|
2327
|
+
op_lower = 0
|
|
2328
|
+
block_lower = created
|
|
2329
|
+
op_upper = max_index
|
|
2330
|
+
block_upper = latest_blocknum
|
|
2331
|
+
last_op_num = None
|
|
2332
|
+
cnt = 0
|
|
2333
|
+
|
|
2334
|
+
while True:
|
|
2335
|
+
# check if the maximum number of iterations was reached
|
|
2336
|
+
if max_count != -1 and cnt >= max_count:
|
|
2337
|
+
# did not converge, return the current state
|
|
2338
|
+
return op_num
|
|
2339
|
+
|
|
2340
|
+
# linear approximation between the known upper and
|
|
2341
|
+
# lower bounds for the first iteration
|
|
2342
|
+
if cnt < 1:
|
|
2343
|
+
op_num = int(
|
|
2344
|
+
(target_blocknum - block_lower)
|
|
2345
|
+
/ (block_upper - block_lower)
|
|
2346
|
+
* (op_upper - op_lower)
|
|
2347
|
+
+ op_lower
|
|
2348
|
+
)
|
|
2349
|
+
else:
|
|
2350
|
+
# divide and conquer for the following iterations
|
|
2351
|
+
op_num = int((op_upper + op_lower) / 2)
|
|
2352
|
+
if op_upper == op_lower + 1: # round up if we're close to target
|
|
2353
|
+
op_num += 1
|
|
2354
|
+
|
|
2355
|
+
# get block number for current op number estimation
|
|
2356
|
+
if op_num != last_op_num:
|
|
2357
|
+
block_num = self._get_blocknum_from_hist(op_num, min_index=min_index)
|
|
2358
|
+
while block_num is None and op_num < max_index:
|
|
2359
|
+
op_num += 1
|
|
2360
|
+
block_num = self._get_blocknum_from_hist(op_num, min_index=min_index)
|
|
2361
|
+
last_op_num = op_num
|
|
2362
|
+
|
|
2363
|
+
# check if the required accuracy was reached
|
|
2364
|
+
if op_upper - op_lower <= stop_diff or op_upper == op_lower + 1:
|
|
2365
|
+
return op_num
|
|
2366
|
+
|
|
2367
|
+
# set new upper/lower boundaries for next iteration
|
|
2368
|
+
if block_num < target_blocknum:
|
|
2369
|
+
# current op number was too low -> search upwards
|
|
2370
|
+
op_lower = op_num
|
|
2371
|
+
block_lower = block_num
|
|
2372
|
+
else:
|
|
2373
|
+
# current op number was too high or matched the target block
|
|
2374
|
+
# -> search downwards
|
|
2375
|
+
op_upper = op_num
|
|
2376
|
+
block_upper = block_num
|
|
2377
|
+
cnt += 1
|
|
2378
|
+
|
|
2379
|
+
def get_curation_reward(self, days=7):
|
|
2380
|
+
"""Returns the curation reward of the last `days` days
|
|
2381
|
+
|
|
2382
|
+
:param int days: limit number of days to be included int the return value
|
|
2383
|
+
"""
|
|
2384
|
+
stop = addTzInfo(datetime.now(timezone.utc)) - timedelta(days=days)
|
|
2385
|
+
reward_vests = Amount(
|
|
2386
|
+
0, self.blockchain.vest_token_symbol, blockchain_instance=self.blockchain
|
|
2387
|
+
)
|
|
2388
|
+
for reward in self.history_reverse(
|
|
2389
|
+
stop=stop, use_block_num=False, only_ops=["curation_reward"]
|
|
2390
|
+
):
|
|
2391
|
+
reward_vests += Amount(reward["reward"], blockchain_instance=self.blockchain)
|
|
2392
|
+
return self.blockchain.vests_to_token_power(float(reward_vests))
|
|
2393
|
+
|
|
2394
|
+
def curation_stats(self):
|
|
2395
|
+
"""Returns the curation reward of the last 24h and 7d and the average
|
|
2396
|
+
of the last 7 days
|
|
2397
|
+
|
|
2398
|
+
:returns: Account curation
|
|
2399
|
+
:rtype: dictionary
|
|
2400
|
+
|
|
2401
|
+
Sample output:
|
|
2402
|
+
|
|
2403
|
+
.. code-block:: js
|
|
2404
|
+
|
|
2405
|
+
{
|
|
2406
|
+
'24hr': 0.0,
|
|
2407
|
+
'7d': 0.0,
|
|
2408
|
+
'avg': 0.0
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
"""
|
|
2412
|
+
return {
|
|
2413
|
+
"24hr": self.get_curation_reward(days=1),
|
|
2414
|
+
"7d": self.get_curation_reward(days=7),
|
|
2415
|
+
"avg": self.get_curation_reward(days=7) / 7,
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
def _get_operation_filter(self, only_ops=[], exclude_ops=[]):
|
|
2419
|
+
from nectarbase.operationids import operations
|
|
2420
|
+
|
|
2421
|
+
operation_filter_low = 0
|
|
2422
|
+
operation_filter_high = 0
|
|
2423
|
+
if len(only_ops) == 0 and len(exclude_ops) == 0:
|
|
2424
|
+
return None, None
|
|
2425
|
+
if len(only_ops) > 0:
|
|
2426
|
+
for op in only_ops:
|
|
2427
|
+
op_id = operations[op]
|
|
2428
|
+
if op_id <= 64:
|
|
2429
|
+
operation_filter_low += 2**op_id
|
|
2430
|
+
else:
|
|
2431
|
+
operation_filter_high += 2 ** (op_id - 64 - 1)
|
|
2432
|
+
else:
|
|
2433
|
+
for op in operations:
|
|
2434
|
+
op_id = operations[op]
|
|
2435
|
+
if op_id <= 64:
|
|
2436
|
+
operation_filter_low += 2**op_id
|
|
2437
|
+
else:
|
|
2438
|
+
operation_filter_high += 2 ** (op_id - 64 - 1)
|
|
2439
|
+
for op in exclude_ops:
|
|
2440
|
+
op_id = operations[op]
|
|
2441
|
+
if op_id <= 64:
|
|
2442
|
+
operation_filter_low -= 2**op_id
|
|
2443
|
+
else:
|
|
2444
|
+
operation_filter_high -= 2 ** (op_id - 64 - 1)
|
|
2445
|
+
return operation_filter_low, operation_filter_high
|
|
2446
|
+
|
|
2447
|
+
def get_account_history(
|
|
2448
|
+
self,
|
|
2449
|
+
index,
|
|
2450
|
+
limit,
|
|
2451
|
+
order=-1,
|
|
2452
|
+
start=None,
|
|
2453
|
+
stop=None,
|
|
2454
|
+
use_block_num=True,
|
|
2455
|
+
only_ops=[],
|
|
2456
|
+
exclude_ops=[],
|
|
2457
|
+
raw_output=False,
|
|
2458
|
+
):
|
|
2459
|
+
"""Returns a generator for individual account transactions. This call can be used in a
|
|
2460
|
+
``for`` loop.
|
|
2461
|
+
|
|
2462
|
+
:param int index: first number of transactions to return
|
|
2463
|
+
:param int limit: limit number of transactions to return
|
|
2464
|
+
:param start: start number/date of transactions to
|
|
2465
|
+
return (*optional*)
|
|
2466
|
+
:type start: int, datetime
|
|
2467
|
+
:param stop: stop number/date of transactions to
|
|
2468
|
+
return (*optional*)
|
|
2469
|
+
:type stop: int, datetime
|
|
2470
|
+
:param bool use_block_num: if true, start and stop are block numbers, otherwise virtual OP count numbers.
|
|
2471
|
+
:param array only_ops: Limit generator by these
|
|
2472
|
+
operations (*optional*)
|
|
2473
|
+
:param array exclude_ops: Exclude thse operations from
|
|
2474
|
+
generator (*optional*)
|
|
2475
|
+
:param int batch_size: internal api call batch size (*optional*)
|
|
2476
|
+
:param int order: 1 for chronological, -1 for reverse order
|
|
2477
|
+
:param bool raw_output: if False, the output is a dict, which
|
|
2478
|
+
includes all values. Otherwise, the output is list.
|
|
2479
|
+
|
|
2480
|
+
.. note::
|
|
2481
|
+
|
|
2482
|
+
only_ops and exclude_ops takes an array of strings:
|
|
2483
|
+
The full list of operation ID's can be found in
|
|
2484
|
+
nectarbase.operationids.ops.
|
|
2485
|
+
Example: ['transfer', 'vote']
|
|
2486
|
+
|
|
2487
|
+
"""
|
|
2488
|
+
if order != -1 and order != 1:
|
|
2489
|
+
raise ValueError("order must be -1 or 1!")
|
|
2490
|
+
# self.blockchain.rpc.set_next_node_on_empty_reply(True)
|
|
2491
|
+
operation_filter_low = None
|
|
2492
|
+
operation_filter_high = None
|
|
2493
|
+
if self.blockchain.rpc.url == "https://api.hive.blog":
|
|
2494
|
+
operation_filter_low, operation_filter_high = self._get_operation_filter(
|
|
2495
|
+
only_ops=only_ops, exclude_ops=exclude_ops
|
|
2496
|
+
)
|
|
2497
|
+
try:
|
|
2498
|
+
txs = self._get_account_history(
|
|
2499
|
+
start=index,
|
|
2500
|
+
limit=limit,
|
|
2501
|
+
operation_filter_low=operation_filter_low,
|
|
2502
|
+
operation_filter_high=operation_filter_high,
|
|
2503
|
+
)
|
|
2504
|
+
except FilteredItemNotFound:
|
|
2505
|
+
txs = []
|
|
2506
|
+
if txs is None:
|
|
2507
|
+
return
|
|
2508
|
+
start = addTzInfo(start)
|
|
2509
|
+
stop = addTzInfo(stop)
|
|
2510
|
+
|
|
2511
|
+
if order == -1:
|
|
2512
|
+
txs_list = reversed(txs)
|
|
2513
|
+
else:
|
|
2514
|
+
txs_list = txs
|
|
2515
|
+
for item in txs_list:
|
|
2516
|
+
item_index, event = item
|
|
2517
|
+
if start and isinstance(start, (datetime, date, time)):
|
|
2518
|
+
timediff = start - formatTimeString(event["timestamp"])
|
|
2519
|
+
if timediff.total_seconds() * float(order) > 0:
|
|
2520
|
+
continue
|
|
2521
|
+
elif start is not None and use_block_num and order == 1 and event["block"] < start:
|
|
2522
|
+
continue
|
|
2523
|
+
elif start is not None and use_block_num and order == -1 and event["block"] > start:
|
|
2524
|
+
continue
|
|
2525
|
+
elif start is not None and not use_block_num and order == 1 and item_index < start:
|
|
2526
|
+
continue
|
|
2527
|
+
elif start is not None and not use_block_num and order == -1 and item_index > start:
|
|
2528
|
+
continue
|
|
2529
|
+
if stop is not None and isinstance(stop, (datetime, date, time)):
|
|
2530
|
+
timediff = stop - formatTimeString(event["timestamp"])
|
|
2531
|
+
if timediff.total_seconds() * float(order) < 0:
|
|
2532
|
+
return
|
|
2533
|
+
elif stop is not None and use_block_num and order == 1 and event["block"] > stop:
|
|
2534
|
+
return
|
|
2535
|
+
elif stop is not None and use_block_num and order == -1 and event["block"] < stop:
|
|
2536
|
+
return
|
|
2537
|
+
elif stop is not None and not use_block_num and order == 1 and item_index > stop:
|
|
2538
|
+
return
|
|
2539
|
+
elif stop is not None and not use_block_num and order == -1 and item_index < stop:
|
|
2540
|
+
return
|
|
2541
|
+
|
|
2542
|
+
if isinstance(event["op"], list):
|
|
2543
|
+
op_type, op = event["op"]
|
|
2544
|
+
else:
|
|
2545
|
+
op_type = event["op"]["type"]
|
|
2546
|
+
if len(op_type) > 10 and op_type[len(op_type) - 10 :] == "_operation":
|
|
2547
|
+
op_type = op_type[:-10]
|
|
2548
|
+
op = event["op"]["value"]
|
|
2549
|
+
block_props = remove_from_dict(event, keys=["op"], keep_keys=False)
|
|
2550
|
+
|
|
2551
|
+
def construct_op(account_name):
|
|
2552
|
+
# verbatim output from steemd
|
|
2553
|
+
if raw_output:
|
|
2554
|
+
return item
|
|
2555
|
+
|
|
2556
|
+
# index can change during reindexing in
|
|
2557
|
+
# future hard-forks. Thus we cannot take it for granted.
|
|
2558
|
+
immutable = op.copy()
|
|
2559
|
+
immutable.update(block_props)
|
|
2560
|
+
immutable.update(
|
|
2561
|
+
{
|
|
2562
|
+
"account": account_name,
|
|
2563
|
+
"type": op_type,
|
|
2564
|
+
}
|
|
2565
|
+
)
|
|
2566
|
+
_id = Blockchain.hash_op(immutable)
|
|
2567
|
+
immutable.update(
|
|
2568
|
+
{
|
|
2569
|
+
"_id": _id,
|
|
2570
|
+
"index": item_index,
|
|
2571
|
+
}
|
|
2572
|
+
)
|
|
2573
|
+
return immutable
|
|
2574
|
+
|
|
2575
|
+
if exclude_ops and op_type in exclude_ops:
|
|
2576
|
+
continue
|
|
2577
|
+
if not only_ops or op_type in only_ops:
|
|
2578
|
+
yield construct_op(self["name"])
|
|
2579
|
+
|
|
2580
|
+
def history(
|
|
2581
|
+
self,
|
|
2582
|
+
start=None,
|
|
2583
|
+
stop=None,
|
|
2584
|
+
use_block_num=True,
|
|
2585
|
+
only_ops=[],
|
|
2586
|
+
exclude_ops=[],
|
|
2587
|
+
batch_size=1000,
|
|
2588
|
+
raw_output=False,
|
|
2589
|
+
):
|
|
2590
|
+
"""Returns a generator for individual account transactions. The
|
|
2591
|
+
earlist operation will be first. This call can be used in a
|
|
2592
|
+
``for`` loop.
|
|
2593
|
+
|
|
2594
|
+
:param start: start number/date of transactions to return (*optional*)
|
|
2595
|
+
:type start: int, datetime
|
|
2596
|
+
:param stop: stop number/date of transactions to return (*optional*)
|
|
2597
|
+
:type stop: int, datetime
|
|
2598
|
+
:param bool use_block_num: if true, start and stop are block numbers,
|
|
2599
|
+
otherwise virtual OP count numbers.
|
|
2600
|
+
:param array only_ops: Limit generator by these
|
|
2601
|
+
operations (*optional*)
|
|
2602
|
+
:param array exclude_ops: Exclude thse operations from
|
|
2603
|
+
generator (*optional*)
|
|
2604
|
+
:param int batch_size: internal api call batch size (*optional*)
|
|
2605
|
+
:param bool raw_output: if False, the output is a dict, which
|
|
2606
|
+
includes all values. Otherwise, the output is list.
|
|
2607
|
+
|
|
2608
|
+
.. note::
|
|
2609
|
+
only_ops and exclude_ops takes an array of strings:
|
|
2610
|
+
The full list of operation ID's can be found in
|
|
2611
|
+
nectarbase.operationids.ops.
|
|
2612
|
+
Example: ['transfer', 'vote']
|
|
2613
|
+
|
|
2614
|
+
.. testsetup::
|
|
2615
|
+
|
|
2616
|
+
from nectar.account import Account
|
|
2617
|
+
from datetime import datetime
|
|
2618
|
+
|
|
2619
|
+
.. testcode::
|
|
2620
|
+
|
|
2621
|
+
acc = Account("gtg")
|
|
2622
|
+
max_op_count = acc.virtual_op_count()
|
|
2623
|
+
# Returns the 100 latest operations
|
|
2624
|
+
acc_op = []
|
|
2625
|
+
for h in acc.history(start=max_op_count - 99, stop=max_op_count, use_block_num=False):
|
|
2626
|
+
acc_op.append(h)
|
|
2627
|
+
len(acc_op)
|
|
2628
|
+
|
|
2629
|
+
.. testoutput::
|
|
2630
|
+
|
|
2631
|
+
100
|
|
2632
|
+
|
|
2633
|
+
.. testcode::
|
|
2634
|
+
|
|
2635
|
+
acc = Account("test")
|
|
2636
|
+
max_block = 21990141
|
|
2637
|
+
# Returns the account operation inside the last 100 block. This can be empty.
|
|
2638
|
+
acc_op = []
|
|
2639
|
+
for h in acc.history(start=max_block - 99, stop=max_block, use_block_num=True):
|
|
2640
|
+
acc_op.append(h)
|
|
2641
|
+
len(acc_op)
|
|
2642
|
+
|
|
2643
|
+
.. testoutput::
|
|
2644
|
+
|
|
2645
|
+
0
|
|
2646
|
+
|
|
2647
|
+
.. testcode::
|
|
2648
|
+
|
|
2649
|
+
acc = Account("test")
|
|
2650
|
+
start_time = datetime(2018, 3, 1, 0, 0, 0)
|
|
2651
|
+
stop_time = datetime(2018, 3, 2, 0, 0, 0)
|
|
2652
|
+
# Returns the account operation from 1.4.2018 back to 1.3.2018
|
|
2653
|
+
acc_op = []
|
|
2654
|
+
for h in acc.history(start=start_time, stop=stop_time):
|
|
2655
|
+
acc_op.append(h)
|
|
2656
|
+
len(acc_op)
|
|
2657
|
+
|
|
2658
|
+
.. testoutput::
|
|
2659
|
+
|
|
2660
|
+
0
|
|
2661
|
+
|
|
2662
|
+
"""
|
|
2663
|
+
_limit = batch_size
|
|
2664
|
+
max_index = self.virtual_op_count()
|
|
2665
|
+
if not max_index:
|
|
2666
|
+
return
|
|
2667
|
+
start = addTzInfo(start)
|
|
2668
|
+
stop = addTzInfo(stop)
|
|
2669
|
+
if (
|
|
2670
|
+
start is not None
|
|
2671
|
+
and not use_block_num
|
|
2672
|
+
and not isinstance(start, (datetime, date, time))
|
|
2673
|
+
):
|
|
2674
|
+
start_index = start
|
|
2675
|
+
elif start is not None and max_index > batch_size:
|
|
2676
|
+
created, min_index = self._get_first_blocknum()
|
|
2677
|
+
op_est = self.estimate_virtual_op_num(start, stop_diff=1, min_index=min_index)
|
|
2678
|
+
if op_est < min_index:
|
|
2679
|
+
op_est = min_index
|
|
2680
|
+
est_diff = 0
|
|
2681
|
+
if isinstance(start, (datetime, date, time)):
|
|
2682
|
+
for h in self.get_account_history(op_est, 0):
|
|
2683
|
+
block_date = formatTimeString(h["timestamp"])
|
|
2684
|
+
while op_est > est_diff + batch_size and block_date > start:
|
|
2685
|
+
est_diff += batch_size
|
|
2686
|
+
if op_est - est_diff < 0:
|
|
2687
|
+
est_diff = op_est
|
|
2688
|
+
for h in self.get_account_history(op_est - est_diff, 0):
|
|
2689
|
+
block_date = formatTimeString(h["timestamp"])
|
|
2690
|
+
elif not isinstance(start, (datetime, date, time)):
|
|
2691
|
+
for h in self.get_account_history(op_est, 0):
|
|
2692
|
+
block_num = h["block"]
|
|
2693
|
+
while op_est > est_diff + batch_size and block_num > start:
|
|
2694
|
+
est_diff += batch_size
|
|
2695
|
+
if op_est - est_diff < 0:
|
|
2696
|
+
est_diff = op_est
|
|
2697
|
+
for h in self.get_account_history(op_est - est_diff, 0):
|
|
2698
|
+
block_num = h["block"]
|
|
2699
|
+
start_index = op_est - est_diff
|
|
2700
|
+
else:
|
|
2701
|
+
start_index = 0
|
|
2702
|
+
|
|
2703
|
+
if stop is not None and not use_block_num and not isinstance(stop, (datetime, date, time)):
|
|
2704
|
+
if start_index + stop < _limit:
|
|
2705
|
+
_limit = stop
|
|
2706
|
+
|
|
2707
|
+
first = start_index + _limit - 1
|
|
2708
|
+
if first > max_index:
|
|
2709
|
+
_limit = max_index - start_index
|
|
2710
|
+
first = start_index + _limit - 1
|
|
2711
|
+
elif first < _limit and self.blockchain.rpc.url == "https://api.hive.blog":
|
|
2712
|
+
first = _limit - 1
|
|
2713
|
+
elif first < _limit and self.blockchain.rpc.url != "https://api.hive.blog":
|
|
2714
|
+
first = _limit
|
|
2715
|
+
last_round = False
|
|
2716
|
+
|
|
2717
|
+
if _limit < 0:
|
|
2718
|
+
return
|
|
2719
|
+
last_item_index = -1
|
|
2720
|
+
|
|
2721
|
+
if self.blockchain.rpc.url == "https://api.hive.blog" and (
|
|
2722
|
+
len(only_ops) > 0 or len(exclude_ops) > 0
|
|
2723
|
+
):
|
|
2724
|
+
operation_filter = True
|
|
2725
|
+
else:
|
|
2726
|
+
operation_filter = False
|
|
2727
|
+
|
|
2728
|
+
while True:
|
|
2729
|
+
# RPC call
|
|
2730
|
+
if first < _limit - 1 and self.blockchain.rpc.url == "https://api.hive.blog":
|
|
2731
|
+
first = _limit - 1
|
|
2732
|
+
elif first < _limit and self.blockchain.rpc.url != "https://api.hive.blog":
|
|
2733
|
+
first = _limit
|
|
2734
|
+
batch_count = 0
|
|
2735
|
+
for item in self.get_account_history(
|
|
2736
|
+
first,
|
|
2737
|
+
_limit,
|
|
2738
|
+
start=None,
|
|
2739
|
+
stop=None,
|
|
2740
|
+
order=1,
|
|
2741
|
+
only_ops=only_ops,
|
|
2742
|
+
exclude_ops=exclude_ops,
|
|
2743
|
+
raw_output=raw_output,
|
|
2744
|
+
):
|
|
2745
|
+
batch_count += 1
|
|
2746
|
+
if raw_output:
|
|
2747
|
+
item_index, event = item
|
|
2748
|
+
op_type, op = event["op"]
|
|
2749
|
+
timestamp = event["timestamp"]
|
|
2750
|
+
block_num = event["block"]
|
|
2751
|
+
else:
|
|
2752
|
+
item_index = item["index"]
|
|
2753
|
+
op_type = item["type"]
|
|
2754
|
+
timestamp = item["timestamp"]
|
|
2755
|
+
block_num = item["block"]
|
|
2756
|
+
if start is not None and isinstance(start, (datetime, date, time)):
|
|
2757
|
+
timediff = start - formatTimeString(timestamp)
|
|
2758
|
+
if timediff.total_seconds() > 0:
|
|
2759
|
+
continue
|
|
2760
|
+
elif start is not None and use_block_num and block_num < start:
|
|
2761
|
+
continue
|
|
2762
|
+
elif start is not None and not use_block_num and item_index < start:
|
|
2763
|
+
continue
|
|
2764
|
+
elif last_item_index >= item_index:
|
|
2765
|
+
continue
|
|
2766
|
+
if stop is not None and isinstance(stop, (datetime, date, time)):
|
|
2767
|
+
timediff = stop - formatTimeString(timestamp)
|
|
2768
|
+
if timediff.total_seconds() < 0:
|
|
2769
|
+
first = max_index + _limit
|
|
2770
|
+
return
|
|
2771
|
+
elif stop is not None and use_block_num and block_num > stop:
|
|
2772
|
+
return
|
|
2773
|
+
elif stop is not None and not use_block_num and item_index > stop:
|
|
2774
|
+
return
|
|
2775
|
+
if operation_filter:
|
|
2776
|
+
yield item
|
|
2777
|
+
else:
|
|
2778
|
+
if exclude_ops and op_type in exclude_ops:
|
|
2779
|
+
continue
|
|
2780
|
+
if not only_ops or op_type in only_ops:
|
|
2781
|
+
yield item
|
|
2782
|
+
last_item_index = item_index
|
|
2783
|
+
if first < max_index and first + _limit >= max_index and not last_round:
|
|
2784
|
+
_limit = max_index - first
|
|
2785
|
+
first = max_index
|
|
2786
|
+
last_round = True
|
|
2787
|
+
else:
|
|
2788
|
+
if (
|
|
2789
|
+
operation_filter
|
|
2790
|
+
and batch_count < _limit
|
|
2791
|
+
and first + 2000 < max_index
|
|
2792
|
+
and _limit == 1000
|
|
2793
|
+
):
|
|
2794
|
+
first += 2000
|
|
2795
|
+
else:
|
|
2796
|
+
first += _limit
|
|
2797
|
+
if (
|
|
2798
|
+
stop is not None
|
|
2799
|
+
and not use_block_num
|
|
2800
|
+
and isinstance(stop, int)
|
|
2801
|
+
and first >= stop + _limit + 1
|
|
2802
|
+
):
|
|
2803
|
+
break
|
|
2804
|
+
elif first > max_index or last_round:
|
|
2805
|
+
break
|
|
2806
|
+
|
|
2807
|
+
def history_reverse(
|
|
2808
|
+
self,
|
|
2809
|
+
start=None,
|
|
2810
|
+
stop=None,
|
|
2811
|
+
use_block_num=True,
|
|
2812
|
+
only_ops=[],
|
|
2813
|
+
exclude_ops=[],
|
|
2814
|
+
batch_size=1000,
|
|
2815
|
+
raw_output=False,
|
|
2816
|
+
):
|
|
2817
|
+
"""Returns a generator for individual account transactions. The
|
|
2818
|
+
latest operation will be first. This call can be used in a
|
|
2819
|
+
``for`` loop.
|
|
2820
|
+
|
|
2821
|
+
:param start: start number/date of transactions to
|
|
2822
|
+
return. If negative the virtual_op_count is added. (*optional*)
|
|
2823
|
+
:type start: int, datetime
|
|
2824
|
+
:param stop: stop number/date of transactions to
|
|
2825
|
+
return. If negative the virtual_op_count is added. (*optional*)
|
|
2826
|
+
:type stop: int, datetime
|
|
2827
|
+
:param bool use_block_num: if true, start and stop are block numbers,
|
|
2828
|
+
otherwise virtual OP count numbers.
|
|
2829
|
+
:param array only_ops: Limit generator by these
|
|
2830
|
+
operations (*optional*)
|
|
2831
|
+
:param array exclude_ops: Exclude thse operations from
|
|
2832
|
+
generator (*optional*)
|
|
2833
|
+
:param int batch_size: internal api call batch size (*optional*)
|
|
2834
|
+
:param bool raw_output: if False, the output is a dict, which
|
|
2835
|
+
includes all values. Otherwise, the output is list.
|
|
2836
|
+
|
|
2837
|
+
.. note::
|
|
2838
|
+
only_ops and exclude_ops takes an array of strings:
|
|
2839
|
+
The full list of operation ID's can be found in
|
|
2840
|
+
nectarbase.operationids.ops.
|
|
2841
|
+
Example: ['transfer', 'vote']
|
|
2842
|
+
|
|
2843
|
+
.. testsetup::
|
|
2844
|
+
|
|
2845
|
+
from nectar.account import Account
|
|
2846
|
+
from datetime import datetime
|
|
2847
|
+
|
|
2848
|
+
.. testcode::
|
|
2849
|
+
|
|
2850
|
+
acc = Account("gtg")
|
|
2851
|
+
max_op_count = acc.virtual_op_count()
|
|
2852
|
+
# Returns the 100 latest operations
|
|
2853
|
+
acc_op = []
|
|
2854
|
+
for h in acc.history_reverse(start=max_op_count, stop=max_op_count - 99, use_block_num=False):
|
|
2855
|
+
acc_op.append(h)
|
|
2856
|
+
len(acc_op)
|
|
2857
|
+
|
|
2858
|
+
.. testoutput::
|
|
2859
|
+
|
|
2860
|
+
100
|
|
2861
|
+
|
|
2862
|
+
.. testcode::
|
|
2863
|
+
|
|
2864
|
+
max_block = 21990141
|
|
2865
|
+
acc = Account("test")
|
|
2866
|
+
# Returns the account operation inside the last 100 block. This can be empty.
|
|
2867
|
+
acc_op = []
|
|
2868
|
+
for h in acc.history_reverse(start=max_block, stop=max_block-100, use_block_num=True):
|
|
2869
|
+
acc_op.append(h)
|
|
2870
|
+
len(acc_op)
|
|
2871
|
+
|
|
2872
|
+
.. testoutput::
|
|
2873
|
+
|
|
2874
|
+
0
|
|
2875
|
+
|
|
2876
|
+
.. testcode::
|
|
2877
|
+
|
|
2878
|
+
acc = Account("test")
|
|
2879
|
+
start_time = datetime(2018, 4, 1, 0, 0, 0)
|
|
2880
|
+
stop_time = datetime(2018, 3, 1, 0, 0, 0)
|
|
2881
|
+
# Returns the account operation from 1.4.2018 back to 1.3.2018
|
|
2882
|
+
acc_op = []
|
|
2883
|
+
for h in acc.history_reverse(start=start_time, stop=stop_time):
|
|
2884
|
+
acc_op.append(h)
|
|
2885
|
+
len(acc_op)
|
|
2886
|
+
|
|
2887
|
+
.. testoutput::
|
|
2888
|
+
|
|
2889
|
+
0
|
|
2890
|
+
|
|
2891
|
+
"""
|
|
2892
|
+
_limit = batch_size
|
|
2893
|
+
first = self.virtual_op_count()
|
|
2894
|
+
start = addTzInfo(start)
|
|
2895
|
+
stop = addTzInfo(stop)
|
|
2896
|
+
if not first or not batch_size:
|
|
2897
|
+
return
|
|
2898
|
+
if start is not None and isinstance(start, int) and start < 0 and not use_block_num:
|
|
2899
|
+
start += first
|
|
2900
|
+
elif start is not None and isinstance(start, int) and not use_block_num:
|
|
2901
|
+
first = start
|
|
2902
|
+
elif start is not None and first > batch_size:
|
|
2903
|
+
created, min_index = self._get_first_blocknum()
|
|
2904
|
+
op_est = self.estimate_virtual_op_num(start, stop_diff=1, min_index=min_index)
|
|
2905
|
+
est_diff = 0
|
|
2906
|
+
if op_est < min_index:
|
|
2907
|
+
op_est = min_index
|
|
2908
|
+
if isinstance(start, (datetime, date, time)):
|
|
2909
|
+
for h in self.get_account_history(op_est, 0):
|
|
2910
|
+
block_date = formatTimeString(h["timestamp"])
|
|
2911
|
+
while op_est + est_diff + batch_size < first and block_date < start:
|
|
2912
|
+
est_diff += batch_size
|
|
2913
|
+
if op_est + est_diff > first:
|
|
2914
|
+
est_diff = first - op_est
|
|
2915
|
+
for h in self.get_account_history(op_est + est_diff, 0):
|
|
2916
|
+
block_date = formatTimeString(h["timestamp"])
|
|
2917
|
+
else:
|
|
2918
|
+
for h in self.get_account_history(op_est, 0):
|
|
2919
|
+
block_num = h["block"]
|
|
2920
|
+
while op_est + est_diff + batch_size < first and block_num < start:
|
|
2921
|
+
est_diff += batch_size
|
|
2922
|
+
if op_est + est_diff > first:
|
|
2923
|
+
est_diff = first - op_est
|
|
2924
|
+
for h in self.get_account_history(op_est + est_diff, 0):
|
|
2925
|
+
block_num = h["block"]
|
|
2926
|
+
first = op_est + est_diff
|
|
2927
|
+
if stop is not None and isinstance(stop, int) and stop < 0 and not use_block_num:
|
|
2928
|
+
stop += first
|
|
2929
|
+
|
|
2930
|
+
if self.blockchain.rpc.url == "https://api.hive.blog" and (
|
|
2931
|
+
len(only_ops) > 0 or len(exclude_ops) > 0
|
|
2932
|
+
):
|
|
2933
|
+
operation_filter = True
|
|
2934
|
+
else:
|
|
2935
|
+
operation_filter = False
|
|
2936
|
+
|
|
2937
|
+
last_item_index = first + 1
|
|
2938
|
+
while True:
|
|
2939
|
+
# RPC call
|
|
2940
|
+
if first - _limit < 0 and self.blockchain.rpc.url == "https://api.hive.blog":
|
|
2941
|
+
_limit = first + 1
|
|
2942
|
+
elif first - _limit < 0 and self.blockchain.rpc.url != "https://api.hive.blog":
|
|
2943
|
+
_limit = first
|
|
2944
|
+
batch_count = 0
|
|
2945
|
+
for item in self.get_account_history(
|
|
2946
|
+
first,
|
|
2947
|
+
_limit,
|
|
2948
|
+
start=None,
|
|
2949
|
+
stop=None,
|
|
2950
|
+
order=-1,
|
|
2951
|
+
only_ops=only_ops,
|
|
2952
|
+
exclude_ops=exclude_ops,
|
|
2953
|
+
raw_output=raw_output,
|
|
2954
|
+
):
|
|
2955
|
+
batch_count += 1
|
|
2956
|
+
if raw_output:
|
|
2957
|
+
item_index, event = item
|
|
2958
|
+
op_type, op = event["op"]
|
|
2959
|
+
timestamp = event["timestamp"]
|
|
2960
|
+
block_num = event["block"]
|
|
2961
|
+
else:
|
|
2962
|
+
item_index = item["index"]
|
|
2963
|
+
op_type = item["type"]
|
|
2964
|
+
timestamp = item["timestamp"]
|
|
2965
|
+
block_num = item["block"]
|
|
2966
|
+
if start is not None and isinstance(start, (datetime, date, time)):
|
|
2967
|
+
timediff = start - formatTimeString(timestamp)
|
|
2968
|
+
if timediff.total_seconds() < 0:
|
|
2969
|
+
continue
|
|
2970
|
+
elif start is not None and use_block_num and block_num > start:
|
|
2971
|
+
continue
|
|
2972
|
+
elif start is not None and not use_block_num and item_index > start:
|
|
2973
|
+
continue
|
|
2974
|
+
elif last_item_index <= item_index:
|
|
2975
|
+
continue
|
|
2976
|
+
if stop is not None and isinstance(stop, (datetime, date, time)):
|
|
2977
|
+
timediff = stop - formatTimeString(timestamp)
|
|
2978
|
+
if timediff.total_seconds() > 0:
|
|
2979
|
+
first = 0
|
|
2980
|
+
return
|
|
2981
|
+
elif stop is not None and use_block_num and block_num < stop:
|
|
2982
|
+
first = 0
|
|
2983
|
+
return
|
|
2984
|
+
elif stop is not None and not use_block_num and item_index < stop:
|
|
2985
|
+
first = 0
|
|
2986
|
+
return
|
|
2987
|
+
if operation_filter:
|
|
2988
|
+
yield item
|
|
2989
|
+
else:
|
|
2990
|
+
if exclude_ops and op_type in exclude_ops:
|
|
2991
|
+
continue
|
|
2992
|
+
if not only_ops or op_type in only_ops:
|
|
2993
|
+
yield item
|
|
2994
|
+
last_item_index = item_index
|
|
2995
|
+
if operation_filter and batch_count < _limit and _limit == 1000:
|
|
2996
|
+
first -= 2000
|
|
2997
|
+
else:
|
|
2998
|
+
first -= _limit
|
|
2999
|
+
if first < 1:
|
|
3000
|
+
break
|
|
3001
|
+
|
|
3002
|
+
def mute(self, mute, account=None):
|
|
3003
|
+
"""Mute another account
|
|
3004
|
+
|
|
3005
|
+
:param str mute: Mute this account
|
|
3006
|
+
:param str account: (optional) the account to allow access
|
|
3007
|
+
to (defaults to ``default_account``)
|
|
3008
|
+
|
|
3009
|
+
"""
|
|
3010
|
+
return self.follow(mute, what=["ignore"], account=account)
|
|
3011
|
+
|
|
3012
|
+
def unfollow(self, unfollow, account=None):
|
|
3013
|
+
"""Unfollow/Unmute another account's blog
|
|
3014
|
+
|
|
3015
|
+
:param str unfollow: Unfollow/Unmute this account
|
|
3016
|
+
:param str account: (optional) the account to allow access
|
|
3017
|
+
to (defaults to ``default_account``)
|
|
3018
|
+
|
|
3019
|
+
"""
|
|
3020
|
+
return self.follow(unfollow, what=[], account=account)
|
|
3021
|
+
|
|
3022
|
+
def follow(self, other, what=["blog"], account=None):
|
|
3023
|
+
"""Follow/Unfollow/Mute/Unmute another account's blog
|
|
3024
|
+
|
|
3025
|
+
.. note:: what can be one of the following on HIVE:
|
|
3026
|
+
blog, ignore, blacklist, unblacklist, follow_blacklist,
|
|
3027
|
+
unfollow_blacklist, follow_muted, unfollow_muted
|
|
3028
|
+
|
|
3029
|
+
:param str/list other: Follow this account / accounts (only hive)
|
|
3030
|
+
:param list what: List of states to follow.
|
|
3031
|
+
``['blog']`` means to follow ``other``,
|
|
3032
|
+
``[]`` means to unfollow/unmute ``other``,
|
|
3033
|
+
``['ignore']`` means to ignore ``other``,
|
|
3034
|
+
(defaults to ``['blog']``)
|
|
3035
|
+
:param str account: (optional) the account to allow access
|
|
3036
|
+
to (defaults to ``default_account``)
|
|
3037
|
+
|
|
3038
|
+
"""
|
|
3039
|
+
if account is None:
|
|
3040
|
+
account = self["name"]
|
|
3041
|
+
account = extract_account_name(account)
|
|
3042
|
+
if not account:
|
|
3043
|
+
raise ValueError("You need to provide an account")
|
|
3044
|
+
if not other:
|
|
3045
|
+
raise ValueError("You need to provide an account to follow/unfollow/mute/unmute")
|
|
3046
|
+
if isinstance(other, str) and other.find(",") > 0:
|
|
3047
|
+
other = other.split(",")
|
|
3048
|
+
json_body = ["follow", {"follower": account, "following": other, "what": what}]
|
|
3049
|
+
return self.blockchain.custom_json("follow", json_body, required_posting_auths=[account])
|
|
3050
|
+
|
|
3051
|
+
def update_account_profile(self, profile, account=None, **kwargs):
|
|
3052
|
+
"""Update an account's profile in json_metadata
|
|
3053
|
+
|
|
3054
|
+
:param dict profile: The new profile to use
|
|
3055
|
+
:param str account: (optional) the account to allow access
|
|
3056
|
+
to (defaults to ``default_account``)
|
|
3057
|
+
|
|
3058
|
+
Sample profile structure:
|
|
3059
|
+
|
|
3060
|
+
.. code-block:: js
|
|
3061
|
+
|
|
3062
|
+
{
|
|
3063
|
+
'name': 'Holger',
|
|
3064
|
+
'about': 'nectar Developer',
|
|
3065
|
+
'location': 'Germany',
|
|
3066
|
+
'profile_image': 'https://c1.staticflickr.com/5/4715/38733717165_7070227c89_n.jpg',
|
|
3067
|
+
'cover_image': 'https://farm1.staticflickr.com/894/26382750057_69f5c8e568.jpg',
|
|
3068
|
+
'website': 'https://github.com/holgern/nectar'
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
.. code-block:: python
|
|
3072
|
+
|
|
3073
|
+
from nectar.account import Account
|
|
3074
|
+
account = Account("test")
|
|
3075
|
+
profile = account.profile
|
|
3076
|
+
profile["about"] = "test account"
|
|
3077
|
+
account.update_account_profile(profile)
|
|
3078
|
+
|
|
3079
|
+
"""
|
|
3080
|
+
if account is None:
|
|
3081
|
+
account = self
|
|
3082
|
+
else:
|
|
3083
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3084
|
+
|
|
3085
|
+
if not isinstance(profile, dict):
|
|
3086
|
+
raise ValueError("Profile must be a dict type!")
|
|
3087
|
+
|
|
3088
|
+
if self["json_metadata"] == "":
|
|
3089
|
+
metadata = {}
|
|
3090
|
+
else:
|
|
3091
|
+
metadata = json.loads(self["json_metadata"])
|
|
3092
|
+
metadata["profile"] = profile
|
|
3093
|
+
return self.update_account_metadata(metadata)
|
|
3094
|
+
|
|
3095
|
+
def update_account_metadata(self, metadata, account=None, **kwargs):
|
|
3096
|
+
"""Update an account's profile in json_metadata
|
|
3097
|
+
|
|
3098
|
+
:param dict metadata: The new metadata to use
|
|
3099
|
+
:param str account: (optional) the account to allow access
|
|
3100
|
+
to (defaults to ``default_account``)
|
|
3101
|
+
|
|
3102
|
+
"""
|
|
3103
|
+
if account is None:
|
|
3104
|
+
account = self
|
|
3105
|
+
else:
|
|
3106
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3107
|
+
if isinstance(metadata, dict):
|
|
3108
|
+
metadata = json.dumps(metadata)
|
|
3109
|
+
elif not isinstance(metadata, str):
|
|
3110
|
+
raise ValueError("Profile must be a dict or string!")
|
|
3111
|
+
op = operations.Account_update(
|
|
3112
|
+
**{
|
|
3113
|
+
"account": account["name"],
|
|
3114
|
+
"memo_key": account["memo_key"],
|
|
3115
|
+
"json_metadata": metadata,
|
|
3116
|
+
"prefix": self.blockchain.prefix,
|
|
3117
|
+
}
|
|
3118
|
+
)
|
|
3119
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3120
|
+
|
|
3121
|
+
def update_account_jsonmetadata(self, metadata, account=None, **kwargs):
|
|
3122
|
+
"""Update an account's profile in json_metadata using the posting key
|
|
3123
|
+
|
|
3124
|
+
:param dict metadata: The new metadata to use
|
|
3125
|
+
:param str account: (optional) the account to allow access
|
|
3126
|
+
to (defaults to ``default_account``)
|
|
3127
|
+
|
|
3128
|
+
"""
|
|
3129
|
+
if account is None:
|
|
3130
|
+
account = self
|
|
3131
|
+
else:
|
|
3132
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3133
|
+
if isinstance(metadata, dict):
|
|
3134
|
+
metadata = json.dumps(metadata)
|
|
3135
|
+
elif not isinstance(metadata, str):
|
|
3136
|
+
raise ValueError("Profile must be a dict or string!")
|
|
3137
|
+
op = operations.Account_update2(
|
|
3138
|
+
**{
|
|
3139
|
+
"account": account["name"],
|
|
3140
|
+
"posting_json_metadata": metadata,
|
|
3141
|
+
"prefix": self.blockchain.prefix,
|
|
3142
|
+
}
|
|
3143
|
+
)
|
|
3144
|
+
return self.blockchain.finalizeOp(op, account, "posting", **kwargs)
|
|
3145
|
+
|
|
3146
|
+
# -------------------------------------------------------------------------
|
|
3147
|
+
# Approval and Disapproval of witnesses
|
|
3148
|
+
# -------------------------------------------------------------------------
|
|
3149
|
+
def approvewitness(self, witness, account=None, approve=True, **kwargs):
|
|
3150
|
+
"""Approve a witness
|
|
3151
|
+
|
|
3152
|
+
:param list witness: list of Witness name or id
|
|
3153
|
+
:param str account: (optional) the account to allow access
|
|
3154
|
+
to (defaults to ``default_account``)
|
|
3155
|
+
|
|
3156
|
+
"""
|
|
3157
|
+
if account is None:
|
|
3158
|
+
account = self
|
|
3159
|
+
else:
|
|
3160
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3161
|
+
|
|
3162
|
+
# if not isinstance(witnesses, (list, set, tuple)):
|
|
3163
|
+
# witnesses = {witnesses}
|
|
3164
|
+
|
|
3165
|
+
# for witness in witnesses:
|
|
3166
|
+
# witness = Witness(witness, blockchain_instance=self)
|
|
3167
|
+
|
|
3168
|
+
op = operations.Account_witness_vote(
|
|
3169
|
+
**{
|
|
3170
|
+
"account": account["name"],
|
|
3171
|
+
"witness": witness,
|
|
3172
|
+
"approve": approve,
|
|
3173
|
+
"prefix": self.blockchain.prefix,
|
|
3174
|
+
}
|
|
3175
|
+
)
|
|
3176
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3177
|
+
|
|
3178
|
+
def disapprovewitness(self, witness, account=None, **kwargs):
|
|
3179
|
+
"""Disapprove a witness
|
|
3180
|
+
|
|
3181
|
+
:param list witness: list of Witness name or id
|
|
3182
|
+
:param str account: (optional) the account to allow access
|
|
3183
|
+
to (defaults to ``default_account``)
|
|
3184
|
+
"""
|
|
3185
|
+
return self.approvewitness(witness=witness, account=account, approve=False)
|
|
3186
|
+
|
|
3187
|
+
def setproxy(self, proxy="", account=None):
|
|
3188
|
+
"""Set the witness and proposal system proxy of an account
|
|
3189
|
+
|
|
3190
|
+
:param proxy: The account to set the proxy to (Leave empty for removing the proxy)
|
|
3191
|
+
:type proxy: str or Account
|
|
3192
|
+
:param account: The account the proxy should be set for
|
|
3193
|
+
:type account: str or Account
|
|
3194
|
+
"""
|
|
3195
|
+
if account is None:
|
|
3196
|
+
account = self
|
|
3197
|
+
elif isinstance(account, Account):
|
|
3198
|
+
pass
|
|
3199
|
+
else:
|
|
3200
|
+
account = Account(account)
|
|
3201
|
+
|
|
3202
|
+
if isinstance(proxy, str):
|
|
3203
|
+
proxy_name = proxy
|
|
3204
|
+
else:
|
|
3205
|
+
proxy_name = proxy["name"]
|
|
3206
|
+
op = operations.Account_witness_proxy(**{"account": account.name, "proxy": proxy_name})
|
|
3207
|
+
return self.blockchain.finalizeOp(op, account, "active")
|
|
3208
|
+
|
|
3209
|
+
def update_memo_key(self, key, account=None, **kwargs):
|
|
3210
|
+
"""Update an account's memo public key
|
|
3211
|
+
|
|
3212
|
+
This method does **not** add any private keys to your
|
|
3213
|
+
wallet but merely changes the memo public key.
|
|
3214
|
+
|
|
3215
|
+
:param str key: New memo public key
|
|
3216
|
+
:param str account: (optional) the account to allow access
|
|
3217
|
+
to (defaults to ``default_account``)
|
|
3218
|
+
"""
|
|
3219
|
+
if account is None:
|
|
3220
|
+
account = self
|
|
3221
|
+
else:
|
|
3222
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3223
|
+
|
|
3224
|
+
PublicKey(key, prefix=self.blockchain.prefix)
|
|
3225
|
+
|
|
3226
|
+
account["memo_key"] = key
|
|
3227
|
+
op = operations.Account_update(
|
|
3228
|
+
**{
|
|
3229
|
+
"account": account["name"],
|
|
3230
|
+
"memo_key": account["memo_key"],
|
|
3231
|
+
"json_metadata": account["json_metadata"],
|
|
3232
|
+
"prefix": self.blockchain.prefix,
|
|
3233
|
+
}
|
|
3234
|
+
)
|
|
3235
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3236
|
+
|
|
3237
|
+
def update_account_keys(self, new_password, account=None, **kwargs):
|
|
3238
|
+
"""Updates all account keys
|
|
3239
|
+
|
|
3240
|
+
This method does **not** add any private keys to your
|
|
3241
|
+
wallet but merely changes the public keys.
|
|
3242
|
+
|
|
3243
|
+
:param str new_password: is used to derive the owner, active,
|
|
3244
|
+
posting and memo key
|
|
3245
|
+
:param str account: (optional) the account to allow access
|
|
3246
|
+
to (defaults to ``default_account``)
|
|
3247
|
+
"""
|
|
3248
|
+
if account is None:
|
|
3249
|
+
account = self
|
|
3250
|
+
else:
|
|
3251
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3252
|
+
|
|
3253
|
+
key_auths = {}
|
|
3254
|
+
for role in ["owner", "active", "posting", "memo"]:
|
|
3255
|
+
pk = PasswordKey(account["name"], new_password, role=role)
|
|
3256
|
+
key_auths[role] = format(pk.get_public_key(), self.blockchain.prefix)
|
|
3257
|
+
|
|
3258
|
+
op = operations.Account_update(
|
|
3259
|
+
**{
|
|
3260
|
+
"account": account["name"],
|
|
3261
|
+
"owner": {
|
|
3262
|
+
"account_auths": [],
|
|
3263
|
+
"key_auths": [[key_auths["owner"], 1]],
|
|
3264
|
+
"address_auths": [],
|
|
3265
|
+
"weight_threshold": 1,
|
|
3266
|
+
},
|
|
3267
|
+
"active": {
|
|
3268
|
+
"account_auths": [],
|
|
3269
|
+
"key_auths": [[key_auths["active"], 1]],
|
|
3270
|
+
"address_auths": [],
|
|
3271
|
+
"weight_threshold": 1,
|
|
3272
|
+
},
|
|
3273
|
+
"posting": {
|
|
3274
|
+
"account_auths": account["posting"]["account_auths"],
|
|
3275
|
+
"key_auths": [[key_auths["posting"], 1]],
|
|
3276
|
+
"address_auths": [],
|
|
3277
|
+
"weight_threshold": 1,
|
|
3278
|
+
},
|
|
3279
|
+
"memo_key": key_auths["memo"],
|
|
3280
|
+
"json_metadata": account["json_metadata"],
|
|
3281
|
+
"prefix": self.blockchain.prefix,
|
|
3282
|
+
}
|
|
3283
|
+
)
|
|
3284
|
+
|
|
3285
|
+
return self.blockchain.finalizeOp(op, account, "owner", **kwargs)
|
|
3286
|
+
|
|
3287
|
+
def change_recovery_account(self, new_recovery_account, account=None, **kwargs):
|
|
3288
|
+
"""Request a change of the recovery account.
|
|
3289
|
+
|
|
3290
|
+
.. note:: It takes 30 days until the change applies. Another
|
|
3291
|
+
request within this time restarts the 30 day period.
|
|
3292
|
+
Setting the current recovery account again cancels any
|
|
3293
|
+
pending change request.
|
|
3294
|
+
|
|
3295
|
+
:param str new_recovery_account: account name of the new
|
|
3296
|
+
recovery account
|
|
3297
|
+
:param str account: (optional) the account to change the
|
|
3298
|
+
recovery account for (defaults to ``default_account``)
|
|
3299
|
+
|
|
3300
|
+
"""
|
|
3301
|
+
if account is None:
|
|
3302
|
+
account = self
|
|
3303
|
+
else:
|
|
3304
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3305
|
+
# Account() lookup to make sure the new account is valid
|
|
3306
|
+
new_rec_acc = Account(new_recovery_account, blockchain_instance=self.blockchain)
|
|
3307
|
+
op = operations.Change_recovery_account(
|
|
3308
|
+
**{
|
|
3309
|
+
"account_to_recover": account["name"],
|
|
3310
|
+
"new_recovery_account": new_rec_acc["name"],
|
|
3311
|
+
"extensions": [],
|
|
3312
|
+
}
|
|
3313
|
+
)
|
|
3314
|
+
return self.blockchain.finalizeOp(op, account, "owner", **kwargs)
|
|
3315
|
+
|
|
3316
|
+
# -------------------------------------------------------------------------
|
|
3317
|
+
# Simple Transfer
|
|
3318
|
+
# -------------------------------------------------------------------------
|
|
3319
|
+
def transfer(
|
|
3320
|
+
self, to, amount, asset, memo="", skip_account_check=False, account=None, **kwargs
|
|
3321
|
+
):
|
|
3322
|
+
"""Transfer an asset to another account.
|
|
3323
|
+
|
|
3324
|
+
:param str to: Recipient
|
|
3325
|
+
:param float amount: Amount to transfer
|
|
3326
|
+
:param str asset: Asset to transfer
|
|
3327
|
+
:param str memo: (optional) Memo, may begin with `#` for encrypted
|
|
3328
|
+
messaging
|
|
3329
|
+
:param bool skip_account_check: (optional) When True, the receiver
|
|
3330
|
+
account name is not checked to speed up sending multiple transfers in a row
|
|
3331
|
+
:param str account: (optional) the source account for the transfer
|
|
3332
|
+
if not ``default_account``
|
|
3333
|
+
|
|
3334
|
+
|
|
3335
|
+
Transfer example:
|
|
3336
|
+
|
|
3337
|
+
.. code-block:: python
|
|
3338
|
+
|
|
3339
|
+
from nectar.account import Account
|
|
3340
|
+
from nectar import Hive
|
|
3341
|
+
active_wif = "5xxxx"
|
|
3342
|
+
stm = Hive(keys=[active_wif])
|
|
3343
|
+
acc = Account("test", blockchain_instance=stm)
|
|
3344
|
+
acc.transfer("test1", 1, "HIVE", "test")
|
|
3345
|
+
|
|
3346
|
+
"""
|
|
3347
|
+
|
|
3348
|
+
if account is None:
|
|
3349
|
+
account = self
|
|
3350
|
+
elif not skip_account_check:
|
|
3351
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3352
|
+
amount = Amount(amount, asset, blockchain_instance=self.blockchain)
|
|
3353
|
+
if not skip_account_check:
|
|
3354
|
+
to = Account(to, blockchain_instance=self.blockchain)
|
|
3355
|
+
|
|
3356
|
+
to_name = extract_account_name(to)
|
|
3357
|
+
account_name = extract_account_name(account)
|
|
3358
|
+
if memo and memo[0] == "#":
|
|
3359
|
+
from .memo import Memo
|
|
3360
|
+
|
|
3361
|
+
memoObj = Memo(from_account=account, to_account=to, blockchain_instance=self.blockchain)
|
|
3362
|
+
memo = memoObj.encrypt(memo[1:])["message"]
|
|
3363
|
+
|
|
3364
|
+
op = operations.Transfer(
|
|
3365
|
+
**{
|
|
3366
|
+
"amount": amount,
|
|
3367
|
+
"to": to_name,
|
|
3368
|
+
"memo": memo,
|
|
3369
|
+
"from": account_name,
|
|
3370
|
+
"prefix": self.blockchain.prefix,
|
|
3371
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
3372
|
+
}
|
|
3373
|
+
)
|
|
3374
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3375
|
+
|
|
3376
|
+
# -------------------------------------------------------------------------------
|
|
3377
|
+
# Recurring Transfer added in hf25
|
|
3378
|
+
# -------------------------------------------------------------------------------
|
|
3379
|
+
def recurring_transfer(
|
|
3380
|
+
self,
|
|
3381
|
+
to,
|
|
3382
|
+
amount,
|
|
3383
|
+
asset,
|
|
3384
|
+
recurrence,
|
|
3385
|
+
executions,
|
|
3386
|
+
memo="",
|
|
3387
|
+
skip_account_check=False,
|
|
3388
|
+
account=None,
|
|
3389
|
+
**kwargs,
|
|
3390
|
+
):
|
|
3391
|
+
"""Transfer an asset to another account.
|
|
3392
|
+
|
|
3393
|
+
:param str to: Recipient
|
|
3394
|
+
:param float amount: Amount to transfer in each occurence, must have 3 decimal points
|
|
3395
|
+
:param str asset: Asset to transfer
|
|
3396
|
+
:param int recurrence: How often in hours to execute transfer
|
|
3397
|
+
:param int executions: Number of times to recur before stopping execution
|
|
3398
|
+
:param str memo: (optional) Memo, may begin with `#` for encrypted
|
|
3399
|
+
messaging
|
|
3400
|
+
:param bool skip_account_check: (optional) When True, the receiver
|
|
3401
|
+
account name is not checked to speed up sending multiple transfers in a row
|
|
3402
|
+
:param str account: (optional) the source account for the transfer
|
|
3403
|
+
if not ``default_account``
|
|
3404
|
+
|
|
3405
|
+
|
|
3406
|
+
Transfer example:
|
|
3407
|
+
|
|
3408
|
+
.. code-block:: python
|
|
3409
|
+
|
|
3410
|
+
from nectar.account import Account
|
|
3411
|
+
from nectar import Hive
|
|
3412
|
+
active_wif = "5xxxx"
|
|
3413
|
+
stm = Hive(keys=[active_wif])
|
|
3414
|
+
acc = Account("test", blockchain_instance=stm)
|
|
3415
|
+
acc.transfer("test1", 1, "HIVE", 48, 5, "test")
|
|
3416
|
+
|
|
3417
|
+
"""
|
|
3418
|
+
|
|
3419
|
+
if account is None:
|
|
3420
|
+
account = self
|
|
3421
|
+
elif not skip_account_check:
|
|
3422
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3423
|
+
amount = Amount(amount, asset, blockchain_instance=self.blockchain)
|
|
3424
|
+
if not skip_account_check:
|
|
3425
|
+
to = Account(to, blockchain_instance=self.blockchain)
|
|
3426
|
+
|
|
3427
|
+
to_name = extract_account_name(to)
|
|
3428
|
+
account_name = extract_account_name(account)
|
|
3429
|
+
if memo and memo[0] == "#":
|
|
3430
|
+
from .memo import Memo
|
|
3431
|
+
|
|
3432
|
+
memoObj = Memo(from_account=account, to_account=to, blockchain_instance=self.blockchain)
|
|
3433
|
+
memo = memoObj.encrypt(memo[1:])["message"]
|
|
3434
|
+
|
|
3435
|
+
op = operations.Recurring_transfer(
|
|
3436
|
+
**{
|
|
3437
|
+
"amount": amount,
|
|
3438
|
+
"to": to_name,
|
|
3439
|
+
"memo": memo,
|
|
3440
|
+
"from": account_name,
|
|
3441
|
+
"recurrence": recurrence,
|
|
3442
|
+
"executions": executions,
|
|
3443
|
+
"prefix": self.blockchain.prefix,
|
|
3444
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
3445
|
+
}
|
|
3446
|
+
)
|
|
3447
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3448
|
+
|
|
3449
|
+
def transfer_to_vesting(
|
|
3450
|
+
self, amount, to=None, account=None, skip_account_check=False, **kwargs
|
|
3451
|
+
):
|
|
3452
|
+
"""Vest STEEM
|
|
3453
|
+
|
|
3454
|
+
:param float amount: Amount to transfer
|
|
3455
|
+
:param str to: Recipient (optional) if not set equal to account
|
|
3456
|
+
:param str account: (optional) the source account for the transfer
|
|
3457
|
+
if not ``default_account``
|
|
3458
|
+
:param bool skip_account_check: (optional) When True, the receiver
|
|
3459
|
+
account name is not checked to speed up sending multiple transfers in a row
|
|
3460
|
+
"""
|
|
3461
|
+
if account is None:
|
|
3462
|
+
account = self
|
|
3463
|
+
elif not skip_account_check:
|
|
3464
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3465
|
+
amount = self._check_amount(amount, self.blockchain.token_symbol)
|
|
3466
|
+
if to is None and skip_account_check:
|
|
3467
|
+
to = self["name"] # powerup on the same account
|
|
3468
|
+
elif to is None:
|
|
3469
|
+
to = self
|
|
3470
|
+
if not skip_account_check:
|
|
3471
|
+
to = Account(to, blockchain_instance=self.blockchain)
|
|
3472
|
+
to_name = extract_account_name(to)
|
|
3473
|
+
account_name = extract_account_name(account)
|
|
3474
|
+
|
|
3475
|
+
op = operations.Transfer_to_vesting(
|
|
3476
|
+
**{
|
|
3477
|
+
"from": account_name,
|
|
3478
|
+
"to": to_name,
|
|
3479
|
+
"amount": amount,
|
|
3480
|
+
"prefix": self.blockchain.prefix,
|
|
3481
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
3482
|
+
}
|
|
3483
|
+
)
|
|
3484
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3485
|
+
|
|
3486
|
+
def convert(self, amount, account=None, request_id=None):
|
|
3487
|
+
"""Convert SteemDollars to Steem (takes 3.5 days to settle)
|
|
3488
|
+
|
|
3489
|
+
:param float amount: amount of SBD to convert
|
|
3490
|
+
:param str account: (optional) the source account for the transfer
|
|
3491
|
+
if not ``default_account``
|
|
3492
|
+
:param str request_id: (optional) identifier for tracking the
|
|
3493
|
+
conversion`
|
|
3494
|
+
|
|
3495
|
+
"""
|
|
3496
|
+
if account is None:
|
|
3497
|
+
account = self
|
|
3498
|
+
else:
|
|
3499
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3500
|
+
amount = self._check_amount(amount, self.blockchain.backed_token_symbol)
|
|
3501
|
+
if request_id:
|
|
3502
|
+
request_id = int(request_id)
|
|
3503
|
+
else:
|
|
3504
|
+
request_id = random.getrandbits(32)
|
|
3505
|
+
op = operations.Convert(
|
|
3506
|
+
**{
|
|
3507
|
+
"owner": account["name"],
|
|
3508
|
+
"requestid": request_id,
|
|
3509
|
+
"amount": amount,
|
|
3510
|
+
"prefix": self.blockchain.prefix,
|
|
3511
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
3512
|
+
}
|
|
3513
|
+
)
|
|
3514
|
+
|
|
3515
|
+
return self.blockchain.finalizeOp(op, account, "active")
|
|
3516
|
+
|
|
3517
|
+
# Added to differentiate and match the addition of the HF25 convert operation
|
|
3518
|
+
def collateralized_convert(self, amount, account=None, request_id=None, **kwargs):
|
|
3519
|
+
"""Convert Hive dollars to Hive (this method is meant to be more instant)
|
|
3520
|
+
and reflect the method added in HF25
|
|
3521
|
+
|
|
3522
|
+
:param float amount: amount of SBD to convert
|
|
3523
|
+
:param str account: (optional) the source account for the transfer
|
|
3524
|
+
if not ``default_account``
|
|
3525
|
+
:param str request_id: (optional) identifier for tracking the
|
|
3526
|
+
conversion`
|
|
3527
|
+
|
|
3528
|
+
"""
|
|
3529
|
+
if account is None:
|
|
3530
|
+
account = self
|
|
3531
|
+
else:
|
|
3532
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3533
|
+
amount = self._check_amount(amount, self.blockchain.backed_token_symbol)
|
|
3534
|
+
if request_id:
|
|
3535
|
+
request_id = int(request_id)
|
|
3536
|
+
else:
|
|
3537
|
+
request_id = random.getrandbits(32)
|
|
3538
|
+
op = operations.Collateralized_convert(
|
|
3539
|
+
**{
|
|
3540
|
+
"owner": account["name"],
|
|
3541
|
+
"requestid": request_id,
|
|
3542
|
+
"amount": amount,
|
|
3543
|
+
"prefix": self.blockchain.prefix,
|
|
3544
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
3545
|
+
}
|
|
3546
|
+
)
|
|
3547
|
+
|
|
3548
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3549
|
+
|
|
3550
|
+
def transfer_to_savings(self, amount, asset, memo, to=None, account=None, **kwargs):
|
|
3551
|
+
"""Transfer SBD or STEEM into a 'savings' account.
|
|
3552
|
+
|
|
3553
|
+
:param float amount: STEEM or SBD amount
|
|
3554
|
+
:param float asset: 'STEEM' or 'SBD'
|
|
3555
|
+
:param str memo: (optional) Memo
|
|
3556
|
+
:param str to: (optional) the source account for the transfer if
|
|
3557
|
+
not ``default_account``
|
|
3558
|
+
:param str account: (optional) the source account for the transfer
|
|
3559
|
+
if not ``default_account``
|
|
3560
|
+
|
|
3561
|
+
"""
|
|
3562
|
+
if asset not in [self.blockchain.token_symbol, self.blockchain.backed_token_symbol]:
|
|
3563
|
+
raise AssertionError()
|
|
3564
|
+
|
|
3565
|
+
if account is None:
|
|
3566
|
+
account = self
|
|
3567
|
+
else:
|
|
3568
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3569
|
+
|
|
3570
|
+
amount = Amount(amount, asset, blockchain_instance=self.blockchain)
|
|
3571
|
+
if to is None:
|
|
3572
|
+
to = account # move to savings on same account
|
|
3573
|
+
else:
|
|
3574
|
+
to = Account(to, blockchain_instance=self.blockchain)
|
|
3575
|
+
op = operations.Transfer_to_savings(
|
|
3576
|
+
**{
|
|
3577
|
+
"from": account["name"],
|
|
3578
|
+
"to": to["name"],
|
|
3579
|
+
"amount": amount,
|
|
3580
|
+
"memo": memo,
|
|
3581
|
+
"prefix": self.blockchain.prefix,
|
|
3582
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
3583
|
+
}
|
|
3584
|
+
)
|
|
3585
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3586
|
+
|
|
3587
|
+
def transfer_from_savings(
|
|
3588
|
+
self, amount, asset, memo, request_id=None, to=None, account=None, **kwargs
|
|
3589
|
+
):
|
|
3590
|
+
"""Withdraw SBD or STEEM from 'savings' account.
|
|
3591
|
+
|
|
3592
|
+
:param float amount: STEEM or SBD amount
|
|
3593
|
+
:param float asset: 'STEEM' or 'SBD'
|
|
3594
|
+
:param str memo: (optional) Memo
|
|
3595
|
+
:param str request_id: (optional) identifier for tracking or
|
|
3596
|
+
cancelling the withdrawal
|
|
3597
|
+
:param str to: (optional) the source account for the transfer if
|
|
3598
|
+
not ``default_account``
|
|
3599
|
+
:param str account: (optional) the source account for the transfer
|
|
3600
|
+
if not ``default_account``
|
|
3601
|
+
|
|
3602
|
+
"""
|
|
3603
|
+
if asset not in [self.blockchain.token_symbol, self.blockchain.backed_token_symbol]:
|
|
3604
|
+
raise AssertionError()
|
|
3605
|
+
|
|
3606
|
+
if account is None:
|
|
3607
|
+
account = self
|
|
3608
|
+
else:
|
|
3609
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3610
|
+
if to is None:
|
|
3611
|
+
to = account # move to savings on same account
|
|
3612
|
+
else:
|
|
3613
|
+
to = Account(to, blockchain_instance=self.blockchain)
|
|
3614
|
+
amount = Amount(amount, asset, blockchain_instance=self.blockchain)
|
|
3615
|
+
if request_id:
|
|
3616
|
+
request_id = int(request_id)
|
|
3617
|
+
else:
|
|
3618
|
+
request_id = random.getrandbits(32)
|
|
3619
|
+
|
|
3620
|
+
op = operations.Transfer_from_savings(
|
|
3621
|
+
**{
|
|
3622
|
+
"from": account["name"],
|
|
3623
|
+
"request_id": request_id,
|
|
3624
|
+
"to": to["name"],
|
|
3625
|
+
"amount": amount,
|
|
3626
|
+
"memo": memo,
|
|
3627
|
+
"prefix": self.blockchain.prefix,
|
|
3628
|
+
"json_str": not bool(self.blockchain.config["use_condenser"]),
|
|
3629
|
+
}
|
|
3630
|
+
)
|
|
3631
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3632
|
+
|
|
3633
|
+
def cancel_transfer_from_savings(self, request_id, account=None, **kwargs):
|
|
3634
|
+
"""Cancel a withdrawal from 'savings' account.
|
|
3635
|
+
|
|
3636
|
+
:param str request_id: Identifier for tracking or cancelling
|
|
3637
|
+
the withdrawal
|
|
3638
|
+
:param str account: (optional) the source account for the transfer
|
|
3639
|
+
if not ``default_account``
|
|
3640
|
+
|
|
3641
|
+
"""
|
|
3642
|
+
if account is None:
|
|
3643
|
+
account = self
|
|
3644
|
+
else:
|
|
3645
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3646
|
+
op = operations.Cancel_transfer_from_savings(
|
|
3647
|
+
**{
|
|
3648
|
+
"from": account["name"],
|
|
3649
|
+
"request_id": request_id,
|
|
3650
|
+
"prefix": self.blockchain.prefix,
|
|
3651
|
+
}
|
|
3652
|
+
)
|
|
3653
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3654
|
+
|
|
3655
|
+
def _check_amount(self, amount, symbol):
|
|
3656
|
+
if isinstance(amount, (float, integer_types)):
|
|
3657
|
+
amount = Amount(amount, symbol, blockchain_instance=self.blockchain)
|
|
3658
|
+
elif (
|
|
3659
|
+
isinstance(amount, string_types)
|
|
3660
|
+
and amount.replace(".", "", 1).replace(",", "", 1).isdigit()
|
|
3661
|
+
):
|
|
3662
|
+
amount = Amount(float(amount), symbol, blockchain_instance=self.blockchain)
|
|
3663
|
+
else:
|
|
3664
|
+
amount = Amount(amount, blockchain_instance=self.blockchain)
|
|
3665
|
+
if not amount["symbol"] == symbol:
|
|
3666
|
+
raise AssertionError()
|
|
3667
|
+
return amount
|
|
3668
|
+
|
|
3669
|
+
def claim_reward_balance(
|
|
3670
|
+
self,
|
|
3671
|
+
reward_steem=0,
|
|
3672
|
+
reward_sbd=0,
|
|
3673
|
+
reward_hive=0,
|
|
3674
|
+
reward_hbd=0,
|
|
3675
|
+
reward_vests=0,
|
|
3676
|
+
account=None,
|
|
3677
|
+
**kwargs,
|
|
3678
|
+
):
|
|
3679
|
+
"""Claim reward balances.
|
|
3680
|
+
By default, this will claim ``all`` outstanding balances. To bypass
|
|
3681
|
+
this behaviour, set desired claim amount by setting any of
|
|
3682
|
+
`reward_steem`/``reward_hive, `reward_sbd`/``reward_hbd or `reward_vests`.
|
|
3683
|
+
|
|
3684
|
+
:param str reward_steem: Amount of STEEM you would like to claim.
|
|
3685
|
+
:param str reward_hive: Amount of HIVE you would like to claim.
|
|
3686
|
+
:param str reward_sbd: Amount of SBD you would like to claim.
|
|
3687
|
+
:param str reward_hbd: Amount of HBD you would like to claim.
|
|
3688
|
+
:param str reward_vests: Amount of VESTS you would like to claim.
|
|
3689
|
+
:param str account: The source account for the claim if not
|
|
3690
|
+
``default_account`` is used.
|
|
3691
|
+
|
|
3692
|
+
"""
|
|
3693
|
+
if account is None:
|
|
3694
|
+
account = self
|
|
3695
|
+
else:
|
|
3696
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3697
|
+
if not account:
|
|
3698
|
+
raise ValueError("You need to provide an account")
|
|
3699
|
+
|
|
3700
|
+
# if no values were set by user, claim all outstanding balances on
|
|
3701
|
+
# account
|
|
3702
|
+
|
|
3703
|
+
reward_token_amount = self._check_amount(
|
|
3704
|
+
reward_steem + reward_hive, self.blockchain.token_symbol
|
|
3705
|
+
)
|
|
3706
|
+
reward_backed_token_amount = self._check_amount(
|
|
3707
|
+
reward_sbd + reward_hbd, self.blockchain.backed_token_symbol
|
|
3708
|
+
)
|
|
3709
|
+
reward_vests_amount = self._check_amount(reward_vests, self.blockchain.vest_token_symbol)
|
|
3710
|
+
|
|
3711
|
+
if self.blockchain.is_hive:
|
|
3712
|
+
reward_token = "reward_hive"
|
|
3713
|
+
reward_backed_token = "reward_hbd"
|
|
3714
|
+
else:
|
|
3715
|
+
reward_token = "reward_steem"
|
|
3716
|
+
reward_backed_token = "reward_sbd"
|
|
3717
|
+
|
|
3718
|
+
if (
|
|
3719
|
+
reward_token_amount.amount == 0
|
|
3720
|
+
and reward_backed_token_amount.amount == 0
|
|
3721
|
+
and reward_vests_amount.amount == 0
|
|
3722
|
+
):
|
|
3723
|
+
if len(account.balances["rewards"]) == 3:
|
|
3724
|
+
reward_token_amount = account.balances["rewards"][0]
|
|
3725
|
+
reward_backed_token_amount = account.balances["rewards"][1]
|
|
3726
|
+
reward_vests_amount = account.balances["rewards"][2]
|
|
3727
|
+
else:
|
|
3728
|
+
reward_token_amount = account.balances["rewards"][0]
|
|
3729
|
+
reward_vests_amount = account.balances["rewards"][1]
|
|
3730
|
+
if len(account.balances["rewards"]) == 3:
|
|
3731
|
+
op = operations.Claim_reward_balance(
|
|
3732
|
+
**{
|
|
3733
|
+
"account": account["name"],
|
|
3734
|
+
reward_token: reward_token_amount,
|
|
3735
|
+
reward_backed_token: reward_backed_token_amount,
|
|
3736
|
+
"reward_vests": reward_vests_amount,
|
|
3737
|
+
"prefix": self.blockchain.prefix,
|
|
3738
|
+
}
|
|
3739
|
+
)
|
|
3740
|
+
else:
|
|
3741
|
+
op = operations.Claim_reward_balance(
|
|
3742
|
+
**{
|
|
3743
|
+
"account": account["name"],
|
|
3744
|
+
reward_token: reward_token_amount,
|
|
3745
|
+
"reward_vests": reward_vests_amount,
|
|
3746
|
+
"prefix": self.blockchain.prefix,
|
|
3747
|
+
}
|
|
3748
|
+
)
|
|
3749
|
+
|
|
3750
|
+
return self.blockchain.finalizeOp(op, account, "posting", **kwargs)
|
|
3751
|
+
|
|
3752
|
+
def delegate_vesting_shares(self, to_account, vesting_shares, account=None, **kwargs):
|
|
3753
|
+
"""Delegate SP to another account.
|
|
3754
|
+
|
|
3755
|
+
:param str to_account: Account we are delegating shares to
|
|
3756
|
+
(delegatee).
|
|
3757
|
+
:param str vesting_shares: Amount of VESTS to delegate eg. `10000
|
|
3758
|
+
VESTS`.
|
|
3759
|
+
:param str account: The source account (delegator). If not specified,
|
|
3760
|
+
``default_account`` is used.
|
|
3761
|
+
"""
|
|
3762
|
+
if account is None:
|
|
3763
|
+
account = self
|
|
3764
|
+
else:
|
|
3765
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3766
|
+
to_account = Account(to_account, blockchain_instance=self.blockchain)
|
|
3767
|
+
if to_account is None:
|
|
3768
|
+
raise ValueError("You need to provide a to_account")
|
|
3769
|
+
vesting_shares = self._check_amount(vesting_shares, self.blockchain.vest_token_symbol)
|
|
3770
|
+
|
|
3771
|
+
op = operations.Delegate_vesting_shares(
|
|
3772
|
+
**{
|
|
3773
|
+
"delegator": account["name"],
|
|
3774
|
+
"delegatee": to_account["name"],
|
|
3775
|
+
"vesting_shares": vesting_shares,
|
|
3776
|
+
"prefix": self.blockchain.prefix,
|
|
3777
|
+
}
|
|
3778
|
+
)
|
|
3779
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3780
|
+
|
|
3781
|
+
def withdraw_vesting(self, amount, account=None, **kwargs):
|
|
3782
|
+
"""Withdraw VESTS from the vesting account.
|
|
3783
|
+
|
|
3784
|
+
:param float amount: number of VESTS to withdraw over a period of
|
|
3785
|
+
13 weeks
|
|
3786
|
+
:param str account: (optional) the source account for the transfer
|
|
3787
|
+
if not ``default_account``
|
|
3788
|
+
|
|
3789
|
+
"""
|
|
3790
|
+
if account is None:
|
|
3791
|
+
account = self
|
|
3792
|
+
else:
|
|
3793
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3794
|
+
amount = self._check_amount(amount, self.blockchain.vest_token_symbol)
|
|
3795
|
+
|
|
3796
|
+
op = operations.Withdraw_vesting(
|
|
3797
|
+
**{
|
|
3798
|
+
"account": account["name"],
|
|
3799
|
+
"vesting_shares": amount,
|
|
3800
|
+
"prefix": self.blockchain.prefix,
|
|
3801
|
+
}
|
|
3802
|
+
)
|
|
3803
|
+
|
|
3804
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3805
|
+
|
|
3806
|
+
def set_withdraw_vesting_route(
|
|
3807
|
+
self, to, percentage=100, account=None, auto_vest=False, **kwargs
|
|
3808
|
+
):
|
|
3809
|
+
"""Set up a vesting withdraw route. When vesting shares are
|
|
3810
|
+
withdrawn, they will be routed to these accounts based on the
|
|
3811
|
+
specified weights.
|
|
3812
|
+
|
|
3813
|
+
:param str to: Recipient of the vesting withdrawal
|
|
3814
|
+
:param float percentage: The percent of the withdraw to go
|
|
3815
|
+
to the 'to' account.
|
|
3816
|
+
:param str account: (optional) the vesting account
|
|
3817
|
+
:param bool auto_vest: Set to true if the 'to' account
|
|
3818
|
+
should receive the VESTS as VESTS, or false if it should
|
|
3819
|
+
receive them as STEEM. (defaults to ``False``)
|
|
3820
|
+
|
|
3821
|
+
"""
|
|
3822
|
+
if account is None:
|
|
3823
|
+
account = self
|
|
3824
|
+
else:
|
|
3825
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3826
|
+
op = operations.Set_withdraw_vesting_route(
|
|
3827
|
+
**{
|
|
3828
|
+
"from_account": account["name"],
|
|
3829
|
+
"to_account": to,
|
|
3830
|
+
"percent": int(percentage * STEEM_1_PERCENT),
|
|
3831
|
+
"auto_vest": auto_vest,
|
|
3832
|
+
}
|
|
3833
|
+
)
|
|
3834
|
+
|
|
3835
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3836
|
+
|
|
3837
|
+
def allow(
|
|
3838
|
+
self, foreign, weight=None, permission="posting", account=None, threshold=None, **kwargs
|
|
3839
|
+
):
|
|
3840
|
+
"""Give additional access to an account by some other public
|
|
3841
|
+
key or account.
|
|
3842
|
+
|
|
3843
|
+
:param str foreign: The foreign account that will obtain access
|
|
3844
|
+
:param int weight: (optional) The weight to use. If not
|
|
3845
|
+
define, the threshold will be used. If the weight is
|
|
3846
|
+
smaller than the threshold, additional signatures will
|
|
3847
|
+
be required. (defaults to threshold)
|
|
3848
|
+
:param str permission: (optional) The actual permission to
|
|
3849
|
+
modify (defaults to ``posting``)
|
|
3850
|
+
:param str account: (optional) the account to allow access
|
|
3851
|
+
to (defaults to ``default_account``)
|
|
3852
|
+
:param int threshold: (optional) The threshold that needs to be
|
|
3853
|
+
reached by signatures to be able to interact
|
|
3854
|
+
"""
|
|
3855
|
+
from copy import deepcopy
|
|
3856
|
+
|
|
3857
|
+
if account is None:
|
|
3858
|
+
account = self
|
|
3859
|
+
else:
|
|
3860
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3861
|
+
|
|
3862
|
+
if permission not in ["owner", "posting", "active"]:
|
|
3863
|
+
raise ValueError("Permission needs to be either 'owner', 'posting', or 'active")
|
|
3864
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3865
|
+
|
|
3866
|
+
if permission not in account:
|
|
3867
|
+
account = Account(account, blockchain_instance=self.blockchain, lazy=False, full=True)
|
|
3868
|
+
account.clear_cache()
|
|
3869
|
+
account.refresh()
|
|
3870
|
+
if permission not in account:
|
|
3871
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3872
|
+
if permission not in account:
|
|
3873
|
+
raise AssertionError("Could not access permission")
|
|
3874
|
+
|
|
3875
|
+
if not weight:
|
|
3876
|
+
weight = account[permission]["weight_threshold"]
|
|
3877
|
+
|
|
3878
|
+
authority = deepcopy(account[permission])
|
|
3879
|
+
try:
|
|
3880
|
+
pubkey = PublicKey(foreign, prefix=self.blockchain.prefix)
|
|
3881
|
+
authority["key_auths"].append([str(pubkey), weight])
|
|
3882
|
+
except:
|
|
3883
|
+
try:
|
|
3884
|
+
foreign_account = Account(foreign, blockchain_instance=self.blockchain)
|
|
3885
|
+
authority["account_auths"].append([foreign_account["name"], weight])
|
|
3886
|
+
except:
|
|
3887
|
+
raise ValueError("Unknown foreign account or invalid public key")
|
|
3888
|
+
if threshold:
|
|
3889
|
+
authority["weight_threshold"] = threshold
|
|
3890
|
+
self.blockchain._test_weights_treshold(authority)
|
|
3891
|
+
|
|
3892
|
+
op = operations.Account_update(
|
|
3893
|
+
**{
|
|
3894
|
+
"account": account["name"],
|
|
3895
|
+
permission: authority,
|
|
3896
|
+
"memo_key": account["memo_key"],
|
|
3897
|
+
"json_metadata": account["json_metadata"],
|
|
3898
|
+
"prefix": self.blockchain.prefix,
|
|
3899
|
+
}
|
|
3900
|
+
)
|
|
3901
|
+
if permission == "owner":
|
|
3902
|
+
return self.blockchain.finalizeOp(op, account, "owner", **kwargs)
|
|
3903
|
+
else:
|
|
3904
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3905
|
+
|
|
3906
|
+
def disallow(self, foreign, permission="posting", account=None, threshold=None, **kwargs):
|
|
3907
|
+
"""Remove additional access to an account by some other public
|
|
3908
|
+
key or account.
|
|
3909
|
+
|
|
3910
|
+
:param str foreign: The foreign account that will obtain access
|
|
3911
|
+
:param str permission: (optional) The actual permission to
|
|
3912
|
+
modify (defaults to ``posting``)
|
|
3913
|
+
:param str account: (optional) the account to allow access
|
|
3914
|
+
to (defaults to ``default_account``)
|
|
3915
|
+
:param int threshold: The threshold that needs to be reached
|
|
3916
|
+
by signatures to be able to interact
|
|
3917
|
+
"""
|
|
3918
|
+
if account is None:
|
|
3919
|
+
account = self
|
|
3920
|
+
else:
|
|
3921
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
3922
|
+
|
|
3923
|
+
if permission not in ["owner", "active", "posting"]:
|
|
3924
|
+
raise ValueError("Permission needs to be either 'owner', 'posting', or 'active")
|
|
3925
|
+
authority = account[permission]
|
|
3926
|
+
|
|
3927
|
+
try:
|
|
3928
|
+
pubkey = PublicKey(foreign, prefix=self.blockchain.prefix)
|
|
3929
|
+
affected_items = list([x for x in authority["key_auths"] if x[0] == str(pubkey)])
|
|
3930
|
+
authority["key_auths"] = list(
|
|
3931
|
+
[x for x in authority["key_auths"] if x[0] != str(pubkey)]
|
|
3932
|
+
)
|
|
3933
|
+
except:
|
|
3934
|
+
try:
|
|
3935
|
+
foreign_account = Account(foreign, blockchain_instance=self.blockchain)
|
|
3936
|
+
affected_items = list(
|
|
3937
|
+
[x for x in authority["account_auths"] if x[0] == foreign_account["name"]]
|
|
3938
|
+
)
|
|
3939
|
+
authority["account_auths"] = list(
|
|
3940
|
+
[x for x in authority["account_auths"] if x[0] != foreign_account["name"]]
|
|
3941
|
+
)
|
|
3942
|
+
except:
|
|
3943
|
+
raise ValueError("Unknown foreign account or unvalid public key")
|
|
3944
|
+
|
|
3945
|
+
if not affected_items:
|
|
3946
|
+
raise ValueError("Changes nothing!")
|
|
3947
|
+
removed_weight = affected_items[0][1]
|
|
3948
|
+
|
|
3949
|
+
# Define threshold
|
|
3950
|
+
if threshold:
|
|
3951
|
+
authority["weight_threshold"] = threshold
|
|
3952
|
+
|
|
3953
|
+
# Correct threshold (at most by the amount removed from the
|
|
3954
|
+
# authority)
|
|
3955
|
+
try:
|
|
3956
|
+
self.blockchain._test_weights_treshold(authority)
|
|
3957
|
+
except:
|
|
3958
|
+
log.critical("The account's threshold will be reduced by %d" % (removed_weight))
|
|
3959
|
+
authority["weight_threshold"] -= removed_weight
|
|
3960
|
+
self.blockchain._test_weights_treshold(authority)
|
|
3961
|
+
|
|
3962
|
+
op = operations.Account_update(
|
|
3963
|
+
**{
|
|
3964
|
+
"account": account["name"],
|
|
3965
|
+
permission: authority,
|
|
3966
|
+
"memo_key": account["memo_key"],
|
|
3967
|
+
"json_metadata": account["json_metadata"],
|
|
3968
|
+
"prefix": self.blockchain.prefix,
|
|
3969
|
+
}
|
|
3970
|
+
)
|
|
3971
|
+
if permission == "owner":
|
|
3972
|
+
return self.blockchain.finalizeOp(op, account, "owner", **kwargs)
|
|
3973
|
+
else:
|
|
3974
|
+
return self.blockchain.finalizeOp(op, account, "active", **kwargs)
|
|
3975
|
+
|
|
3976
|
+
def feed_history(self, limit=None, start_author=None, start_permlink=None, account=None):
|
|
3977
|
+
"""Stream the feed entries of an account in reverse time order.
|
|
3978
|
+
|
|
3979
|
+
.. note:: RPC nodes keep a limited history of entries for the
|
|
3980
|
+
user feed. Older entries may not be available via this
|
|
3981
|
+
call due to these node limitations.
|
|
3982
|
+
|
|
3983
|
+
:param int limit: (optional) stream the latest `limit`
|
|
3984
|
+
feed entries. If unset (default), all available entries
|
|
3985
|
+
are streamed.
|
|
3986
|
+
:param str start_author: (optional) start streaming the
|
|
3987
|
+
replies from this author. `start_permlink=None`
|
|
3988
|
+
(default) starts with the latest available entry.
|
|
3989
|
+
If set, `start_permlink` has to be set as well.
|
|
3990
|
+
:param str start_permlink: (optional) start streaming the
|
|
3991
|
+
replies from this permlink. `start_permlink=None`
|
|
3992
|
+
(default) starts with the latest available entry.
|
|
3993
|
+
If set, `start_author` has to be set as well.
|
|
3994
|
+
:param str account: (optional) the account to get replies
|
|
3995
|
+
to (defaults to ``default_account``)
|
|
3996
|
+
|
|
3997
|
+
comment_history_reverse example:
|
|
3998
|
+
|
|
3999
|
+
.. code-block:: python
|
|
4000
|
+
|
|
4001
|
+
from nectar.account import Account
|
|
4002
|
+
from nectar import Steem
|
|
4003
|
+
from nectar.nodelist import NodeList
|
|
4004
|
+
nodelist = NodeList()
|
|
4005
|
+
nodelist.update_nodes()
|
|
4006
|
+
stm = Steem(node=nodelist.get_hive_nodes())
|
|
4007
|
+
acc = Account("ned", blockchain_instance=stm)
|
|
4008
|
+
for reply in acc.feed_history(limit=10):
|
|
4009
|
+
print(reply)
|
|
4010
|
+
|
|
4011
|
+
"""
|
|
4012
|
+
if limit is not None:
|
|
4013
|
+
if not isinstance(limit, integer_types) or limit <= 0:
|
|
4014
|
+
raise AssertionError("`limit` has to be greater than 0`")
|
|
4015
|
+
if (start_author is None and start_permlink is not None) or (
|
|
4016
|
+
start_author is not None and start_permlink is None
|
|
4017
|
+
):
|
|
4018
|
+
raise AssertionError(
|
|
4019
|
+
"either both or none of `start_author` and `start_permlink` have to be set"
|
|
4020
|
+
)
|
|
4021
|
+
|
|
4022
|
+
if account is None:
|
|
4023
|
+
account = self
|
|
4024
|
+
else:
|
|
4025
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
4026
|
+
feed_count = 0
|
|
4027
|
+
while True:
|
|
4028
|
+
query_limit = 100
|
|
4029
|
+
if limit is not None:
|
|
4030
|
+
query_limit = min(limit - feed_count + 1, query_limit)
|
|
4031
|
+
from .discussions import Discussions_by_feed, Query
|
|
4032
|
+
|
|
4033
|
+
query = Query(
|
|
4034
|
+
start_author=start_author,
|
|
4035
|
+
start_permlink=start_permlink,
|
|
4036
|
+
limit=query_limit,
|
|
4037
|
+
tag=account["name"],
|
|
4038
|
+
)
|
|
4039
|
+
results = Discussions_by_feed(query, blockchain_instance=self.blockchain)
|
|
4040
|
+
if len(results) == 0 or (start_permlink and len(results) == 1):
|
|
4041
|
+
return
|
|
4042
|
+
if feed_count > 0 and start_permlink:
|
|
4043
|
+
results = results[1:] # strip duplicates from previous iteration
|
|
4044
|
+
for entry in results:
|
|
4045
|
+
feed_count += 1
|
|
4046
|
+
yield entry
|
|
4047
|
+
start_permlink = entry["permlink"]
|
|
4048
|
+
start_author = entry["author"]
|
|
4049
|
+
if feed_count == limit:
|
|
4050
|
+
return
|
|
4051
|
+
|
|
4052
|
+
def blog_history(self, limit=None, start=-1, reblogs=True, account=None):
|
|
4053
|
+
"""Stream the blog entries done by an account in reverse time order.
|
|
4054
|
+
|
|
4055
|
+
.. note:: RPC nodes keep a limited history of entries for the
|
|
4056
|
+
user blog. Older blog posts of an account may not be available
|
|
4057
|
+
via this call due to these node limitations.
|
|
4058
|
+
|
|
4059
|
+
:param int limit: (optional) stream the latest `limit`
|
|
4060
|
+
blog entries. If unset (default), all available blog
|
|
4061
|
+
entries are streamed.
|
|
4062
|
+
:param int start: (optional) start streaming the blog
|
|
4063
|
+
entries from this index. `start=-1` (default) starts
|
|
4064
|
+
with the latest available entry.
|
|
4065
|
+
:param bool reblogs: (optional) if set `True` (default)
|
|
4066
|
+
reblogs / resteems are included. If set `False`,
|
|
4067
|
+
reblogs/resteems are omitted.
|
|
4068
|
+
:param str account: (optional) the account to stream blog
|
|
4069
|
+
entries for (defaults to ``default_account``)
|
|
4070
|
+
|
|
4071
|
+
blog_history_reverse example:
|
|
4072
|
+
|
|
4073
|
+
.. code-block:: python
|
|
4074
|
+
|
|
4075
|
+
from nectar.account import Account
|
|
4076
|
+
from nectar import Steem
|
|
4077
|
+
from nectar.nodelist import NodeList
|
|
4078
|
+
nodelist = NodeList()
|
|
4079
|
+
nodelist.update_nodes()
|
|
4080
|
+
stm = Steem(node=nodelist.get_hive_nodes())
|
|
4081
|
+
acc = Account("steemitblog", blockchain_instance=stm)
|
|
4082
|
+
for post in acc.blog_history(limit=10):
|
|
4083
|
+
print(post)
|
|
4084
|
+
|
|
4085
|
+
"""
|
|
4086
|
+
if limit is not None:
|
|
4087
|
+
if not isinstance(limit, integer_types) or limit <= 0:
|
|
4088
|
+
raise AssertionError("`limit` has to be greater than 0`")
|
|
4089
|
+
|
|
4090
|
+
if account is None:
|
|
4091
|
+
account = self
|
|
4092
|
+
else:
|
|
4093
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
4094
|
+
|
|
4095
|
+
post_count = 0
|
|
4096
|
+
start_permlink = None
|
|
4097
|
+
start_author = None
|
|
4098
|
+
while True:
|
|
4099
|
+
query_limit = 100
|
|
4100
|
+
if limit is not None and reblogs:
|
|
4101
|
+
query_limit = min(limit - post_count + 1, query_limit)
|
|
4102
|
+
|
|
4103
|
+
from .discussions import Discussions_by_blog
|
|
4104
|
+
|
|
4105
|
+
query = {
|
|
4106
|
+
"start_author": start_author,
|
|
4107
|
+
"start_permlink": start_permlink,
|
|
4108
|
+
"limit": query_limit,
|
|
4109
|
+
"tag": account["name"],
|
|
4110
|
+
}
|
|
4111
|
+
results = Discussions_by_blog(query, blockchain_instance=self.blockchain)
|
|
4112
|
+
if len(results) == 0 or (start_permlink and len(results) == 1):
|
|
4113
|
+
return
|
|
4114
|
+
if start_permlink:
|
|
4115
|
+
results = results[1:] # strip duplicates from previous iteration
|
|
4116
|
+
for post in results:
|
|
4117
|
+
if post["author"] == "":
|
|
4118
|
+
continue
|
|
4119
|
+
if reblogs or post["author"] == account["name"]:
|
|
4120
|
+
post_count += 1
|
|
4121
|
+
yield post
|
|
4122
|
+
start_permlink = post["permlink"]
|
|
4123
|
+
start_author = post["author"]
|
|
4124
|
+
if post_count == limit:
|
|
4125
|
+
return
|
|
4126
|
+
|
|
4127
|
+
def comment_history(self, limit=None, start_permlink=None, account=None):
|
|
4128
|
+
"""Stream the comments done by an account in reverse time order.
|
|
4129
|
+
|
|
4130
|
+
.. note:: RPC nodes keep a limited history of user comments for the
|
|
4131
|
+
user feed. Older comments may not be available via this
|
|
4132
|
+
call due to these node limitations.
|
|
4133
|
+
|
|
4134
|
+
:param int limit: (optional) stream the latest `limit`
|
|
4135
|
+
comments. If unset (default), all available comments
|
|
4136
|
+
are streamed.
|
|
4137
|
+
:param str start_permlink: (optional) start streaming the
|
|
4138
|
+
comments from this permlink. `start_permlink=None`
|
|
4139
|
+
(default) starts with the latest available entry.
|
|
4140
|
+
:param str account: (optional) the account to stream
|
|
4141
|
+
comments for (defaults to ``default_account``)
|
|
4142
|
+
|
|
4143
|
+
comment_history_reverse example:
|
|
4144
|
+
|
|
4145
|
+
.. code-block:: python
|
|
4146
|
+
|
|
4147
|
+
from nectar.account import Account
|
|
4148
|
+
from nectar import Steem
|
|
4149
|
+
from nectar.nodelist import NodeList
|
|
4150
|
+
nodelist = NodeList()
|
|
4151
|
+
nodelist.update_nodes()
|
|
4152
|
+
stm = Steem(node=nodelist.get_hive_nodes())
|
|
4153
|
+
acc = Account("ned", blockchain_instance=stm)
|
|
4154
|
+
for comment in acc.comment_history(limit=10):
|
|
4155
|
+
print(comment)
|
|
4156
|
+
|
|
4157
|
+
"""
|
|
4158
|
+
if limit is not None:
|
|
4159
|
+
if not isinstance(limit, integer_types) or limit <= 0:
|
|
4160
|
+
raise AssertionError("`limit` has to be greater than 0`")
|
|
4161
|
+
|
|
4162
|
+
if account is None:
|
|
4163
|
+
account = self
|
|
4164
|
+
else:
|
|
4165
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
4166
|
+
|
|
4167
|
+
comment_count = 0
|
|
4168
|
+
while True:
|
|
4169
|
+
query_limit = 100
|
|
4170
|
+
if limit is not None:
|
|
4171
|
+
query_limit = min(limit - comment_count + 1, query_limit)
|
|
4172
|
+
from .discussions import Discussions_by_comments
|
|
4173
|
+
|
|
4174
|
+
query = {
|
|
4175
|
+
"start_author": account["name"],
|
|
4176
|
+
"start_permlink": start_permlink,
|
|
4177
|
+
"limit": query_limit,
|
|
4178
|
+
}
|
|
4179
|
+
results = Discussions_by_comments(query, blockchain_instance=self.blockchain)
|
|
4180
|
+
if len(results) == 0 or (start_permlink and len(results) == 1):
|
|
4181
|
+
return
|
|
4182
|
+
if comment_count > 0 and start_permlink:
|
|
4183
|
+
results = results[1:] # strip duplicates from previous iteration
|
|
4184
|
+
for comment in results:
|
|
4185
|
+
if comment["permlink"] == "":
|
|
4186
|
+
continue
|
|
4187
|
+
comment_count += 1
|
|
4188
|
+
yield comment
|
|
4189
|
+
start_permlink = comment["permlink"]
|
|
4190
|
+
if comment_count == limit:
|
|
4191
|
+
return
|
|
4192
|
+
|
|
4193
|
+
def reply_history(self, limit=None, start_author=None, start_permlink=None, account=None):
|
|
4194
|
+
"""Stream the replies to an account in reverse time order.
|
|
4195
|
+
|
|
4196
|
+
.. note:: RPC nodes keep a limited history of entries for the
|
|
4197
|
+
replies to an author. Older replies to an account may
|
|
4198
|
+
not be available via this call due to
|
|
4199
|
+
these node limitations.
|
|
4200
|
+
|
|
4201
|
+
:param int limit: (optional) stream the latest `limit`
|
|
4202
|
+
replies. If unset (default), all available replies
|
|
4203
|
+
are streamed.
|
|
4204
|
+
:param str start_author: (optional) start streaming the
|
|
4205
|
+
replies from this author. `start_permlink=None`
|
|
4206
|
+
(default) starts with the latest available entry.
|
|
4207
|
+
If set, `start_permlink` has to be set as well.
|
|
4208
|
+
:param str start_permlink: (optional) start streaming the
|
|
4209
|
+
replies from this permlink. `start_permlink=None`
|
|
4210
|
+
(default) starts with the latest available entry.
|
|
4211
|
+
If set, `start_author` has to be set as well.
|
|
4212
|
+
:param str account: (optional) the account to get replies
|
|
4213
|
+
to (defaults to ``default_account``)
|
|
4214
|
+
|
|
4215
|
+
comment_history_reverse example:
|
|
4216
|
+
|
|
4217
|
+
.. code-block:: python
|
|
4218
|
+
|
|
4219
|
+
from nectar.account import Account
|
|
4220
|
+
acc = Account("ned")
|
|
4221
|
+
for reply in acc.reply_history(limit=10):
|
|
4222
|
+
print(reply)
|
|
4223
|
+
|
|
4224
|
+
"""
|
|
4225
|
+
if limit is not None:
|
|
4226
|
+
if not isinstance(limit, integer_types) or limit <= 0:
|
|
4227
|
+
raise AssertionError("`limit` has to be greater than 0`")
|
|
4228
|
+
if (start_author is None and start_permlink is not None) or (
|
|
4229
|
+
start_author is not None and start_permlink is None
|
|
4230
|
+
):
|
|
4231
|
+
raise AssertionError(
|
|
4232
|
+
"either both or none of `start_author` and `start_permlink` have to be set"
|
|
4233
|
+
)
|
|
4234
|
+
|
|
4235
|
+
if account is None:
|
|
4236
|
+
account = self
|
|
4237
|
+
else:
|
|
4238
|
+
account = Account(account, blockchain_instance=self.blockchain)
|
|
4239
|
+
|
|
4240
|
+
if start_author is None:
|
|
4241
|
+
start_author = account["name"]
|
|
4242
|
+
|
|
4243
|
+
reply_count = 0
|
|
4244
|
+
while True:
|
|
4245
|
+
query_limit = 100
|
|
4246
|
+
if limit is not None:
|
|
4247
|
+
query_limit = min(limit - reply_count + 1, query_limit)
|
|
4248
|
+
from .discussions import Replies_by_last_update
|
|
4249
|
+
|
|
4250
|
+
query = {
|
|
4251
|
+
"start_author": start_author,
|
|
4252
|
+
"start_permlink": start_permlink,
|
|
4253
|
+
"limit": query_limit,
|
|
4254
|
+
}
|
|
4255
|
+
results = Replies_by_last_update(query, blockchain_instance=self.blockchain)
|
|
4256
|
+
if len(results) == 0 or (start_permlink and len(results) == 1):
|
|
4257
|
+
return
|
|
4258
|
+
if reply_count > 0 and start_permlink:
|
|
4259
|
+
results = results[1:] # strip duplicates from previous iteration
|
|
4260
|
+
for reply in results:
|
|
4261
|
+
if reply["author"] == "":
|
|
4262
|
+
continue
|
|
4263
|
+
reply_count += 1
|
|
4264
|
+
yield reply
|
|
4265
|
+
start_author = reply["author"]
|
|
4266
|
+
start_permlink = reply["permlink"]
|
|
4267
|
+
if reply_count == limit:
|
|
4268
|
+
return
|
|
4269
|
+
|
|
4270
|
+
|
|
4271
|
+
class AccountsObject(list):
|
|
4272
|
+
def printAsTable(self):
|
|
4273
|
+
t = PrettyTable(["Name"])
|
|
4274
|
+
t.align = "l"
|
|
4275
|
+
for acc in self:
|
|
4276
|
+
t.add_row([acc["name"]])
|
|
4277
|
+
print(t)
|
|
4278
|
+
|
|
4279
|
+
def print_summarize_table(self, tag_type="Follower", return_str=False, **kwargs):
|
|
4280
|
+
t = PrettyTable(["Key", "Value"])
|
|
4281
|
+
t.align = "r"
|
|
4282
|
+
t.add_row([tag_type + " count", str(len(self))])
|
|
4283
|
+
own_mvest = []
|
|
4284
|
+
eff_sp = []
|
|
4285
|
+
rep = []
|
|
4286
|
+
last_vote_h = []
|
|
4287
|
+
last_post_d = []
|
|
4288
|
+
no_vote = 0
|
|
4289
|
+
no_post = 0
|
|
4290
|
+
for f in self:
|
|
4291
|
+
rep.append(f.rep)
|
|
4292
|
+
own_mvest.append(float(f.balances["available"][2]) / 1e6)
|
|
4293
|
+
eff_sp.append(f.get_token_power())
|
|
4294
|
+
last_vote = addTzInfo(datetime.now(timezone.utc)) - (f["last_vote_time"])
|
|
4295
|
+
if last_vote.days >= 365:
|
|
4296
|
+
no_vote += 1
|
|
4297
|
+
else:
|
|
4298
|
+
last_vote_h.append(last_vote.total_seconds() / 60 / 60)
|
|
4299
|
+
last_post = addTzInfo(datetime.now(timezone.utc)) - (f["last_root_post"])
|
|
4300
|
+
if last_post.days >= 365:
|
|
4301
|
+
no_post += 1
|
|
4302
|
+
else:
|
|
4303
|
+
last_post_d.append(last_post.total_seconds() / 60 / 60 / 24)
|
|
4304
|
+
|
|
4305
|
+
t.add_row(["Summed MVest value", "%.2f" % sum(own_mvest)])
|
|
4306
|
+
if len(rep) > 0:
|
|
4307
|
+
t.add_row(["Mean Rep.", "%.2f" % (sum(rep) / len(rep))])
|
|
4308
|
+
t.add_row(["Max Rep.", "%.2f" % (max(rep))])
|
|
4309
|
+
if len(eff_sp) > 0:
|
|
4310
|
+
t.add_row(["Summed eff. SP", "%.2f" % sum(eff_sp)])
|
|
4311
|
+
t.add_row(["Mean eff. SP", "%.2f" % (sum(eff_sp) / len(eff_sp))])
|
|
4312
|
+
t.add_row(["Max eff. SP", "%.2f" % max(eff_sp)])
|
|
4313
|
+
if len(last_vote_h) > 0:
|
|
4314
|
+
t.add_row(
|
|
4315
|
+
["Mean last vote diff in hours", "%.2f" % (sum(last_vote_h) / len(last_vote_h))]
|
|
4316
|
+
)
|
|
4317
|
+
if len(last_post_d) > 0:
|
|
4318
|
+
t.add_row(
|
|
4319
|
+
["Mean last post diff in days", "%.2f" % (sum(last_post_d) / len(last_post_d))]
|
|
4320
|
+
)
|
|
4321
|
+
t.add_row([tag_type + " without vote in 365 days", no_vote])
|
|
4322
|
+
t.add_row([tag_type + " without post in 365 days", no_post])
|
|
4323
|
+
if return_str:
|
|
4324
|
+
return t.get_string(**kwargs)
|
|
4325
|
+
else:
|
|
4326
|
+
print(t.get_string(**kwargs))
|
|
4327
|
+
|
|
4328
|
+
|
|
4329
|
+
class Accounts(AccountsObject):
|
|
4330
|
+
"""Obtain a list of accounts
|
|
4331
|
+
|
|
4332
|
+
:param list name_list: list of accounts to fetch
|
|
4333
|
+
:param int batch_limit: (optional) maximum number of accounts
|
|
4334
|
+
to fetch per call, defaults to 100
|
|
4335
|
+
:param Steem/Hive blockchain_instance: Steem() or Hive() instance to use when
|
|
4336
|
+
accessing a RPCcreator = Account(creator, blockchain_instance=self)
|
|
4337
|
+
"""
|
|
4338
|
+
|
|
4339
|
+
def __init__(
|
|
4340
|
+
self, name_list, batch_limit=100, lazy=False, full=True, blockchain_instance=None, **kwargs
|
|
4341
|
+
):
|
|
4342
|
+
if blockchain_instance is None:
|
|
4343
|
+
if kwargs.get("steem_instance"):
|
|
4344
|
+
blockchain_instance = kwargs["steem_instance"]
|
|
4345
|
+
elif kwargs.get("hive_instance"):
|
|
4346
|
+
blockchain_instance = kwargs["hive_instance"]
|
|
4347
|
+
self.blockchain = blockchain_instance or shared_blockchain_instance()
|
|
4348
|
+
|
|
4349
|
+
if not self.blockchain.is_connected():
|
|
4350
|
+
return
|
|
4351
|
+
accounts = []
|
|
4352
|
+
name_cnt = 0
|
|
4353
|
+
|
|
4354
|
+
while name_cnt < len(name_list):
|
|
4355
|
+
self.blockchain.rpc.set_next_node_on_empty_reply(False)
|
|
4356
|
+
if self.blockchain.rpc.get_use_appbase():
|
|
4357
|
+
accounts += self.blockchain.rpc.find_accounts(
|
|
4358
|
+
{"accounts": name_list[name_cnt : batch_limit + name_cnt]}, api="database"
|
|
4359
|
+
)["accounts"]
|
|
4360
|
+
else:
|
|
4361
|
+
accounts += self.blockchain.rpc.get_accounts(
|
|
4362
|
+
name_list[name_cnt : batch_limit + name_cnt]
|
|
4363
|
+
)
|
|
4364
|
+
name_cnt += batch_limit
|
|
4365
|
+
|
|
4366
|
+
super(Accounts, self).__init__(
|
|
4367
|
+
[
|
|
4368
|
+
Account(x, lazy=lazy, full=full, blockchain_instance=self.blockchain)
|
|
4369
|
+
for x in accounts
|
|
4370
|
+
]
|
|
4371
|
+
)
|