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
|
@@ -0,0 +1,2284 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import ast
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
|
|
10
|
+
from nectar.constants import (
|
|
11
|
+
CURVE_CONSTANT,
|
|
12
|
+
CURVE_CONSTANT_X4,
|
|
13
|
+
SQUARED_CURVE_CONSTANT,
|
|
14
|
+
STEEM_1_PERCENT,
|
|
15
|
+
STEEM_100_PERCENT,
|
|
16
|
+
STEEM_RC_REGEN_TIME,
|
|
17
|
+
STEEM_VOTE_REGENERATION_SECONDS,
|
|
18
|
+
)
|
|
19
|
+
from nectarapi.noderpc import NodeRPC
|
|
20
|
+
from nectarbase import operations
|
|
21
|
+
from nectargraphenebase.account import PrivateKey, PublicKey
|
|
22
|
+
from nectargraphenebase.chains import known_chains
|
|
23
|
+
|
|
24
|
+
from .account import Account
|
|
25
|
+
from .amount import Amount
|
|
26
|
+
from .exceptions import AccountDoesNotExistsException, AccountExistsException
|
|
27
|
+
from .hivesigner import HiveSigner
|
|
28
|
+
from .price import Price
|
|
29
|
+
from .storage import get_default_config_store
|
|
30
|
+
from .transactionbuilder import TransactionBuilder
|
|
31
|
+
from .utils import (
|
|
32
|
+
derive_permlink,
|
|
33
|
+
remove_from_dict,
|
|
34
|
+
resolve_authorperm,
|
|
35
|
+
sanitize_permlink,
|
|
36
|
+
)
|
|
37
|
+
from .version import version as nectar_version
|
|
38
|
+
from .wallet import Wallet
|
|
39
|
+
|
|
40
|
+
log = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BlockChainInstance(object):
|
|
44
|
+
"""Connect to a Graphene network.
|
|
45
|
+
|
|
46
|
+
:param str node: Node to connect to *(optional)*
|
|
47
|
+
:param str rpcuser: RPC user *(optional)*
|
|
48
|
+
:param str rpcpassword: RPC password *(optional)*
|
|
49
|
+
:param bool nobroadcast: Do **not** broadcast a transaction!
|
|
50
|
+
*(optional)*
|
|
51
|
+
:param bool unsigned: Do **not** sign a transaction! *(optional)*
|
|
52
|
+
:param bool debug: Enable Debugging *(optional)*
|
|
53
|
+
:param keys: Predefine the wif keys to shortcut the
|
|
54
|
+
wallet database *(optional)*
|
|
55
|
+
:type keys: array, dict, string
|
|
56
|
+
:param wif: Predefine the wif keys to shortcut the
|
|
57
|
+
wallet database *(optional)*
|
|
58
|
+
:type wif: array, dict, string
|
|
59
|
+
:param bool offline: Boolean to prevent connecting to network (defaults
|
|
60
|
+
to ``False``) *(optional)*
|
|
61
|
+
:param int expiration: Delay in seconds until transactions are supposed
|
|
62
|
+
to expire *(optional)* (default is 30)
|
|
63
|
+
:param str blocking: Wait for broadcasted transactions to be included
|
|
64
|
+
in a block and return full transaction (can be "head" or
|
|
65
|
+
"irreversible")
|
|
66
|
+
:param bool bundle: Do not broadcast transactions right away, but allow
|
|
67
|
+
to bundle operations. It is not possible to send out more than one
|
|
68
|
+
vote operation and more than one comment operation in a single broadcast *(optional)*
|
|
69
|
+
:param bool appbase: Use the new appbase rpc protocol on nodes with version
|
|
70
|
+
0.19.4 or higher. The settings has no effect on nodes with version of 0.19.3 or lower.
|
|
71
|
+
:param int num_retries: Set the maximum number of reconnects to the nodes before
|
|
72
|
+
NumRetriesReached is raised. Disabled for -1. (default is -1)
|
|
73
|
+
:param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5)
|
|
74
|
+
:param int timeout: Timeout setting for https nodes (default is 60)
|
|
75
|
+
:param bool use_sc2: When True, a steemconnect object is created. Can be used for
|
|
76
|
+
broadcast posting op or creating hot_links (default is False)
|
|
77
|
+
:param SteemConnect steemconnect: A SteemConnect object can be set manually, set use_sc2 to True
|
|
78
|
+
:param dict custom_chains: custom chain which should be added to the known chains
|
|
79
|
+
|
|
80
|
+
Three wallet operation modes are possible:
|
|
81
|
+
|
|
82
|
+
* **Wallet Database**: Here, the steemlibs load the keys from the
|
|
83
|
+
locally stored wallet SQLite database (see ``storage.py``).
|
|
84
|
+
To use this mode, simply call ``Steem()`` without the
|
|
85
|
+
``keys`` parameter
|
|
86
|
+
* **Providing Keys**: Here, you can provide the keys for
|
|
87
|
+
your accounts manually. All you need to do is add the wif
|
|
88
|
+
keys for the accounts you want to use as a simple array
|
|
89
|
+
using the ``keys`` parameter to ``Steem()``.
|
|
90
|
+
* **Force keys**: This more is for advanced users and
|
|
91
|
+
requires that you know what you are doing. Here, the
|
|
92
|
+
``keys`` parameter is a dictionary that overwrite the
|
|
93
|
+
``active``, ``owner``, ``posting`` or ``memo`` keys for
|
|
94
|
+
any account. This mode is only used for *foreign*
|
|
95
|
+
signatures!
|
|
96
|
+
|
|
97
|
+
If no node is provided, it will connect to default nodes of
|
|
98
|
+
http://geo.steem.pl. Default settings can be changed with:
|
|
99
|
+
|
|
100
|
+
.. code-block:: python
|
|
101
|
+
|
|
102
|
+
steem = Steem(<host>)
|
|
103
|
+
|
|
104
|
+
where ``<host>`` starts with ``https://``, ``ws://`` or ``wss://``.
|
|
105
|
+
|
|
106
|
+
The purpose of this class it to simplify interaction with
|
|
107
|
+
Steem.
|
|
108
|
+
|
|
109
|
+
The idea is to have a class that allows to do this:
|
|
110
|
+
|
|
111
|
+
.. code-block:: python
|
|
112
|
+
|
|
113
|
+
>>> from nectar import Steem
|
|
114
|
+
>>> steem = Steem()
|
|
115
|
+
>>> print(steem.get_blockchain_version()) # doctest: +SKIP
|
|
116
|
+
|
|
117
|
+
This class also deals with edits, votes and reading content.
|
|
118
|
+
|
|
119
|
+
Example for adding a custom chain:
|
|
120
|
+
|
|
121
|
+
.. code-block:: python
|
|
122
|
+
|
|
123
|
+
from nectar import Steem
|
|
124
|
+
stm = Steem(node=["https://mytstnet.com"], custom_chains={"MYTESTNET":
|
|
125
|
+
{'chain_assets': [{'asset': 'SBD', 'id': 0, 'precision': 3, 'symbol': 'SBD'},
|
|
126
|
+
{'asset': 'STEEM', 'id': 1, 'precision': 3, 'symbol': 'STEEM'},
|
|
127
|
+
{'asset': 'VESTS', 'id': 2, 'precision': 6, 'symbol': 'VESTS'}],
|
|
128
|
+
'chain_id': '79276aea5d4877d9a25892eaa01b0adf019d3e5cb12a97478df3298ccdd01674',
|
|
129
|
+
'min_version': '0.0.0',
|
|
130
|
+
'prefix': 'MTN'}
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(
|
|
137
|
+
self,
|
|
138
|
+
node="",
|
|
139
|
+
rpcuser=None,
|
|
140
|
+
rpcpassword=None,
|
|
141
|
+
debug=False,
|
|
142
|
+
data_refresh_time_seconds=900,
|
|
143
|
+
**kwargs,
|
|
144
|
+
):
|
|
145
|
+
"""Init steem
|
|
146
|
+
|
|
147
|
+
:param str node: Node to connect to *(optional)*
|
|
148
|
+
:param str rpcuser: RPC user *(optional)*
|
|
149
|
+
:param str rpcpassword: RPC password *(optional)*
|
|
150
|
+
:param bool nobroadcast: Do **not** broadcast a transaction!
|
|
151
|
+
*(optional)*
|
|
152
|
+
:param bool unsigned: Do **not** sign a transaction! *(optional)*
|
|
153
|
+
:param bool debug: Enable Debugging *(optional)*
|
|
154
|
+
:param array,dict,string keys: Predefine the wif keys to shortcut the
|
|
155
|
+
wallet database *(optional)*
|
|
156
|
+
:param array,dict,string wif: Predefine the wif keys to shortcut the
|
|
157
|
+
wallet database *(optional)*
|
|
158
|
+
:param bool offline: Boolean to prevent connecting to network (defaults
|
|
159
|
+
to ``False``) *(optional)*
|
|
160
|
+
:param int expiration: Delay in seconds until transactions are supposed
|
|
161
|
+
to expire *(optional)* (default is 30)
|
|
162
|
+
:param str blocking: Wait for broadcast transactions to be included
|
|
163
|
+
in a block and return full transaction (can be "head" or
|
|
164
|
+
"irreversible")
|
|
165
|
+
:param bool bundle: Do not broadcast transactions right away, but allow
|
|
166
|
+
to bundle operations *(optional)*
|
|
167
|
+
:param int num_retries: Set the maximum number of reconnects to the nodes before
|
|
168
|
+
NumRetriesReached is raised. Disabled for -1. (default is -1)
|
|
169
|
+
:param int num_retries_call: Repeat num_retries_call times a rpc call on node error (default is 5)
|
|
170
|
+
:param int timeout: Timeout setting for https nodes (default is 60)
|
|
171
|
+
:param bool use_sc2: When True, a steemconnect object is created. Can be used for broadcast
|
|
172
|
+
posting op or creating hot_links (default is False)
|
|
173
|
+
:param SteemConnect steemconnect: A SteemConnect object can be set manually, set use_sc2 to True
|
|
174
|
+
:param bool use_ledger: When True, a ledger Nano S is used for signing
|
|
175
|
+
:param str path: bip32 path from which the pubkey is derived, when use_ledger is True
|
|
176
|
+
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
self.rpc = None
|
|
180
|
+
self.debug = debug
|
|
181
|
+
|
|
182
|
+
self.offline = bool(kwargs.get("offline", False))
|
|
183
|
+
self.nobroadcast = bool(kwargs.get("nobroadcast", False))
|
|
184
|
+
self.unsigned = bool(kwargs.get("unsigned", False))
|
|
185
|
+
self.expiration = int(kwargs.get("expiration", 30))
|
|
186
|
+
self.bundle = bool(kwargs.get("bundle", False))
|
|
187
|
+
self.steemconnect = kwargs.get("steemconnect", None)
|
|
188
|
+
self.use_sc2 = bool(kwargs.get("use_sc2", False))
|
|
189
|
+
self.hivesigner = kwargs.get("hivesigner", None)
|
|
190
|
+
self.use_hs = bool(kwargs.get("use_hs", False))
|
|
191
|
+
self.blocking = kwargs.get("blocking", False)
|
|
192
|
+
self.custom_chains = kwargs.get("custom_chains", {})
|
|
193
|
+
self.use_ledger = bool(kwargs.get("use_ledger", False))
|
|
194
|
+
self.path = kwargs.get("path", None)
|
|
195
|
+
|
|
196
|
+
# Store config for access through other Classes
|
|
197
|
+
self.config = kwargs.get("config_store", get_default_config_store(**kwargs))
|
|
198
|
+
if self.path is None:
|
|
199
|
+
self.path = self.config["default_path"]
|
|
200
|
+
|
|
201
|
+
if not self.offline:
|
|
202
|
+
self.connect(node=node, rpcuser=rpcuser, rpcpassword=rpcpassword, **kwargs)
|
|
203
|
+
|
|
204
|
+
self.clear_data()
|
|
205
|
+
self.data_refresh_time_seconds = data_refresh_time_seconds
|
|
206
|
+
# self.refresh_data()
|
|
207
|
+
|
|
208
|
+
# txbuffers/propbuffer are initialized and cleared
|
|
209
|
+
self.clear()
|
|
210
|
+
|
|
211
|
+
self.wallet = Wallet(blockchain_instance=self, **kwargs)
|
|
212
|
+
|
|
213
|
+
# set steemconnect
|
|
214
|
+
if self.steemconnect is not None and not isinstance(self.steemconnect, (HiveSigner)):
|
|
215
|
+
raise ValueError("steemconnect musst be SteemConnect object")
|
|
216
|
+
if self.hivesigner is not None and not isinstance(self.hivesigner, (HiveSigner)):
|
|
217
|
+
raise ValueError("hivesigner musst be HiveSigner object")
|
|
218
|
+
if self.steemconnect is not None and not self.use_sc2:
|
|
219
|
+
self.use_sc2 = True
|
|
220
|
+
elif self.hivesigner is None and self.use_hs:
|
|
221
|
+
self.hivesigner = HiveSigner(blockchain_instance=self, **kwargs)
|
|
222
|
+
elif self.hivesigner is not None and not self.use_hs:
|
|
223
|
+
self.use_hs = True
|
|
224
|
+
|
|
225
|
+
# -------------------------------------------------------------------------
|
|
226
|
+
# Basic Calls
|
|
227
|
+
# -------------------------------------------------------------------------
|
|
228
|
+
def connect(self, node="", rpcuser="", rpcpassword="", **kwargs):
|
|
229
|
+
"""Connect to Steem network (internal use only)"""
|
|
230
|
+
if not node:
|
|
231
|
+
node = self.get_default_nodes()
|
|
232
|
+
if not bool(node):
|
|
233
|
+
raise ValueError("A Hive node needs to be provided!")
|
|
234
|
+
|
|
235
|
+
if not rpcuser and "rpcuser" in self.config:
|
|
236
|
+
rpcuser = self.config["rpcuser"]
|
|
237
|
+
|
|
238
|
+
if not rpcpassword and "rpcpassword" in self.config:
|
|
239
|
+
rpcpassword = self.config["rpcpassword"]
|
|
240
|
+
|
|
241
|
+
if "use_tor" in self.config:
|
|
242
|
+
use_tor = self.config["use_tor"]
|
|
243
|
+
else:
|
|
244
|
+
use_tor = False
|
|
245
|
+
|
|
246
|
+
self.rpc = NodeRPC(node, rpcuser, rpcpassword, use_tor=use_tor, **kwargs)
|
|
247
|
+
|
|
248
|
+
def is_connected(self):
|
|
249
|
+
"""Returns if rpc is connected"""
|
|
250
|
+
return self.rpc is not None
|
|
251
|
+
|
|
252
|
+
def __repr__(self):
|
|
253
|
+
if self.offline:
|
|
254
|
+
return "<%s offline=True>" % (self.__class__.__name__)
|
|
255
|
+
elif self.rpc is not None and len(self.rpc.url) > 0:
|
|
256
|
+
return "<%s node=%s, nobroadcast=%s>" % (
|
|
257
|
+
self.__class__.__name__,
|
|
258
|
+
str(self.rpc.url),
|
|
259
|
+
str(self.nobroadcast),
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
return "<%s, nobroadcast=%s>" % (self.__class__.__name__, str(self.nobroadcast))
|
|
263
|
+
|
|
264
|
+
def clear_data(self):
|
|
265
|
+
"""Clears all stored blockchain parameters"""
|
|
266
|
+
self.data = {
|
|
267
|
+
"last_refresh": None,
|
|
268
|
+
"last_node": None,
|
|
269
|
+
"last_refresh_dynamic_global_properties": None,
|
|
270
|
+
"dynamic_global_properties": None,
|
|
271
|
+
"feed_history": None,
|
|
272
|
+
"get_feed_history": None,
|
|
273
|
+
"last_refresh_feed_history": None,
|
|
274
|
+
"hardfork_properties": None,
|
|
275
|
+
"last_refresh_hardfork_properties": None,
|
|
276
|
+
"network": None,
|
|
277
|
+
"last_refresh_network": None,
|
|
278
|
+
"witness_schedule": None,
|
|
279
|
+
"last_refresh_witness_schedule": None,
|
|
280
|
+
"config": None,
|
|
281
|
+
"last_refresh_config": None,
|
|
282
|
+
"reward_funds": None,
|
|
283
|
+
"last_refresh_reward_funds": None,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
def refresh_data(self, chain_property, force_refresh=False, data_refresh_time_seconds=None):
|
|
287
|
+
"""Read and stores steem blockchain parameters
|
|
288
|
+
If the last data refresh is older than data_refresh_time_seconds, data will be refreshed
|
|
289
|
+
|
|
290
|
+
:param bool force_refresh: if True, a refresh of the data is enforced
|
|
291
|
+
:param float data_refresh_time_seconds: set a new minimal refresh time in seconds
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
# if self.offline:
|
|
295
|
+
# return
|
|
296
|
+
if data_refresh_time_seconds is not None:
|
|
297
|
+
self.data_refresh_time_seconds = data_refresh_time_seconds
|
|
298
|
+
if chain_property == "dynamic_global_properties":
|
|
299
|
+
if not self.offline:
|
|
300
|
+
if (
|
|
301
|
+
self.data["last_refresh_dynamic_global_properties"] is not None
|
|
302
|
+
and not force_refresh
|
|
303
|
+
and self.data["last_node"] == self.rpc.url
|
|
304
|
+
):
|
|
305
|
+
if (
|
|
306
|
+
datetime.now(timezone.utc)
|
|
307
|
+
- self.data["last_refresh_dynamic_global_properties"]
|
|
308
|
+
).total_seconds() < self.data_refresh_time_seconds:
|
|
309
|
+
return
|
|
310
|
+
self.data["last_refresh_dynamic_global_properties"] = datetime.now(timezone.utc)
|
|
311
|
+
self.data["last_refresh"] = datetime.now(timezone.utc)
|
|
312
|
+
self.data["last_node"] = self.rpc.url
|
|
313
|
+
self.data["dynamic_global_properties"] = self.get_dynamic_global_properties(False)
|
|
314
|
+
elif chain_property == "feed_history":
|
|
315
|
+
if not self.offline:
|
|
316
|
+
if (
|
|
317
|
+
self.data["last_refresh_feed_history"] is not None
|
|
318
|
+
and not force_refresh
|
|
319
|
+
and self.data["last_node"] == self.rpc.url
|
|
320
|
+
):
|
|
321
|
+
if (
|
|
322
|
+
datetime.now(timezone.utc) - self.data["last_refresh_feed_history"]
|
|
323
|
+
).total_seconds() < self.data_refresh_time_seconds:
|
|
324
|
+
return
|
|
325
|
+
|
|
326
|
+
self.data["last_refresh_feed_history"] = datetime.now(timezone.utc)
|
|
327
|
+
self.data["last_refresh"] = datetime.now(timezone.utc)
|
|
328
|
+
self.data["last_node"] = self.rpc.url
|
|
329
|
+
try:
|
|
330
|
+
self.data["feed_history"] = self.get_feed_history(False)
|
|
331
|
+
except:
|
|
332
|
+
self.data["feed_history"] = None
|
|
333
|
+
self.data["get_feed_history"] = self.data["feed_history"]
|
|
334
|
+
elif chain_property == "hardfork_properties":
|
|
335
|
+
if not self.offline:
|
|
336
|
+
if (
|
|
337
|
+
self.data["last_refresh_hardfork_properties"] is not None
|
|
338
|
+
and not force_refresh
|
|
339
|
+
and self.data["last_node"] == self.rpc.url
|
|
340
|
+
):
|
|
341
|
+
if (
|
|
342
|
+
datetime.now(timezone.utc) - self.data["last_refresh_hardfork_properties"]
|
|
343
|
+
).total_seconds() < self.data_refresh_time_seconds:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
self.data["last_refresh_hardfork_properties"] = datetime.now(timezone.utc)
|
|
347
|
+
self.data["last_refresh"] = datetime.now(timezone.utc)
|
|
348
|
+
self.data["last_node"] = self.rpc.url
|
|
349
|
+
try:
|
|
350
|
+
self.data["hardfork_properties"] = self.get_hardfork_properties(False)
|
|
351
|
+
except:
|
|
352
|
+
self.data["hardfork_properties"] = None
|
|
353
|
+
elif chain_property == "witness_schedule":
|
|
354
|
+
if not self.offline:
|
|
355
|
+
if (
|
|
356
|
+
self.data["last_refresh_witness_schedule"] is not None
|
|
357
|
+
and not force_refresh
|
|
358
|
+
and self.data["last_node"] == self.rpc.url
|
|
359
|
+
):
|
|
360
|
+
if (
|
|
361
|
+
datetime.now(timezone.utc) - self.data["last_refresh_witness_schedule"]
|
|
362
|
+
).total_seconds() < 3:
|
|
363
|
+
return
|
|
364
|
+
self.data["last_refresh_witness_schedule"] = datetime.now(timezone.utc)
|
|
365
|
+
self.data["last_refresh"] = datetime.now(timezone.utc)
|
|
366
|
+
self.data["last_node"] = self.rpc.url
|
|
367
|
+
self.data["witness_schedule"] = self.get_witness_schedule(False)
|
|
368
|
+
elif chain_property == "config":
|
|
369
|
+
if not self.offline:
|
|
370
|
+
if (
|
|
371
|
+
self.data["last_refresh_config"] is not None
|
|
372
|
+
and not force_refresh
|
|
373
|
+
and self.data["last_node"] == self.rpc.url
|
|
374
|
+
):
|
|
375
|
+
if (
|
|
376
|
+
datetime.now(timezone.utc) - self.data["last_refresh_config"]
|
|
377
|
+
).total_seconds() < self.data_refresh_time_seconds:
|
|
378
|
+
return
|
|
379
|
+
self.data["last_refresh_config"] = datetime.now(timezone.utc)
|
|
380
|
+
self.data["last_refresh"] = datetime.now(timezone.utc)
|
|
381
|
+
self.data["last_node"] = self.rpc.url
|
|
382
|
+
self.data["config"] = self.get_config(False)
|
|
383
|
+
self.data["network"] = self.get_network(False, config=self.data["config"])
|
|
384
|
+
elif chain_property == "reward_funds":
|
|
385
|
+
if not self.offline:
|
|
386
|
+
if (
|
|
387
|
+
self.data["last_refresh_reward_funds"] is not None
|
|
388
|
+
and not force_refresh
|
|
389
|
+
and self.data["last_node"] == self.rpc.url
|
|
390
|
+
):
|
|
391
|
+
if (
|
|
392
|
+
datetime.now(timezone.utc) - self.data["last_refresh_reward_funds"]
|
|
393
|
+
).total_seconds() < self.data_refresh_time_seconds:
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
self.data["last_refresh_reward_funds"] = datetime.now(timezone.utc)
|
|
397
|
+
self.data["last_refresh"] = datetime.now(timezone.utc)
|
|
398
|
+
self.data["last_node"] = self.rpc.url
|
|
399
|
+
self.data["reward_funds"] = self.get_reward_funds(False)
|
|
400
|
+
else:
|
|
401
|
+
raise ValueError("%s is not unkown" % str(chain_property))
|
|
402
|
+
|
|
403
|
+
def get_dynamic_global_properties(self, use_stored_data=True):
|
|
404
|
+
"""This call returns the *dynamic global properties*
|
|
405
|
+
|
|
406
|
+
:param bool use_stored_data: if True, stored data will be returned. If stored data are
|
|
407
|
+
empty or old, refresh_data() is used.
|
|
408
|
+
|
|
409
|
+
"""
|
|
410
|
+
if use_stored_data:
|
|
411
|
+
self.refresh_data("dynamic_global_properties")
|
|
412
|
+
return self.data["dynamic_global_properties"]
|
|
413
|
+
if self.rpc is None:
|
|
414
|
+
return None
|
|
415
|
+
self.rpc.set_next_node_on_empty_reply(True)
|
|
416
|
+
return self.rpc.get_dynamic_global_properties(api="database")
|
|
417
|
+
|
|
418
|
+
def get_reserve_ratio(self):
|
|
419
|
+
"""This call returns the *reserve ratio*"""
|
|
420
|
+
if self.rpc is None:
|
|
421
|
+
return None
|
|
422
|
+
self.rpc.set_next_node_on_empty_reply(True)
|
|
423
|
+
|
|
424
|
+
props = self.get_dynamic_global_properties()
|
|
425
|
+
# conf = self.get_config()
|
|
426
|
+
try:
|
|
427
|
+
reserve_ratio = {
|
|
428
|
+
"id": 0,
|
|
429
|
+
"average_block_size": props["average_block_size"],
|
|
430
|
+
"current_reserve_ratio": props["current_reserve_ratio"],
|
|
431
|
+
"max_virtual_bandwidth": props["max_virtual_bandwidth"],
|
|
432
|
+
}
|
|
433
|
+
except:
|
|
434
|
+
reserve_ratio = {
|
|
435
|
+
"id": 0,
|
|
436
|
+
"average_block_size": None,
|
|
437
|
+
"current_reserve_ratio": None,
|
|
438
|
+
"max_virtual_bandwidth": None,
|
|
439
|
+
}
|
|
440
|
+
return reserve_ratio
|
|
441
|
+
|
|
442
|
+
def get_feed_history(self, use_stored_data=True):
|
|
443
|
+
"""Returns the feed_history
|
|
444
|
+
|
|
445
|
+
:param bool use_stored_data: if True, stored data will be returned. If stored data are
|
|
446
|
+
empty or old, refresh_data() is used.
|
|
447
|
+
|
|
448
|
+
"""
|
|
449
|
+
if use_stored_data:
|
|
450
|
+
self.refresh_data("feed_history")
|
|
451
|
+
return self.data["feed_history"]
|
|
452
|
+
if self.rpc is None:
|
|
453
|
+
return None
|
|
454
|
+
self.rpc.set_next_node_on_empty_reply(True)
|
|
455
|
+
return self.rpc.get_feed_history(api="database")
|
|
456
|
+
|
|
457
|
+
def get_reward_funds(self, use_stored_data=True):
|
|
458
|
+
"""Get details for a reward fund.
|
|
459
|
+
|
|
460
|
+
:param bool use_stored_data: if True, stored data will be returned. If stored data are
|
|
461
|
+
empty or old, refresh_data() is used.
|
|
462
|
+
|
|
463
|
+
"""
|
|
464
|
+
if use_stored_data:
|
|
465
|
+
self.refresh_data("reward_funds")
|
|
466
|
+
return self.data["reward_funds"]
|
|
467
|
+
|
|
468
|
+
if self.rpc is None:
|
|
469
|
+
return None
|
|
470
|
+
ret = None
|
|
471
|
+
self.rpc.set_next_node_on_empty_reply(True)
|
|
472
|
+
if self.rpc.get_use_appbase():
|
|
473
|
+
funds = self.rpc.get_reward_funds(api="database")
|
|
474
|
+
if funds is not None:
|
|
475
|
+
funds = funds["funds"]
|
|
476
|
+
else:
|
|
477
|
+
return None
|
|
478
|
+
if len(funds) > 0:
|
|
479
|
+
funds = funds[0]
|
|
480
|
+
ret = funds
|
|
481
|
+
else:
|
|
482
|
+
ret = self.rpc.get_reward_fund("post", api="database")
|
|
483
|
+
return ret
|
|
484
|
+
|
|
485
|
+
def get_current_median_history(self, use_stored_data=True):
|
|
486
|
+
"""Returns the current median price
|
|
487
|
+
|
|
488
|
+
:param bool use_stored_data: if True, stored data will be returned. If stored data are
|
|
489
|
+
empty or old, refresh_data() is used.
|
|
490
|
+
"""
|
|
491
|
+
if use_stored_data:
|
|
492
|
+
self.refresh_data("feed_history")
|
|
493
|
+
if self.data["get_feed_history"]:
|
|
494
|
+
return self.data["get_feed_history"]["current_median_history"]
|
|
495
|
+
else:
|
|
496
|
+
return None
|
|
497
|
+
if self.rpc is None:
|
|
498
|
+
return None
|
|
499
|
+
ret = None
|
|
500
|
+
self.rpc.set_next_node_on_empty_reply(True)
|
|
501
|
+
if self.rpc.get_use_appbase():
|
|
502
|
+
ret = self.rpc.get_feed_history(api="database")["current_median_history"]
|
|
503
|
+
else:
|
|
504
|
+
ret = self.rpc.get_current_median_history_price(api="database")
|
|
505
|
+
return ret
|
|
506
|
+
|
|
507
|
+
def get_hardfork_properties(self, use_stored_data=True):
|
|
508
|
+
"""Returns Hardfork and live_time of the hardfork
|
|
509
|
+
|
|
510
|
+
:param bool use_stored_data: if True, stored data will be returned. If stored data are
|
|
511
|
+
empty or old, refresh_data() is used.
|
|
512
|
+
"""
|
|
513
|
+
if use_stored_data:
|
|
514
|
+
self.refresh_data("hardfork_properties")
|
|
515
|
+
return self.data["hardfork_properties"]
|
|
516
|
+
if self.rpc is None:
|
|
517
|
+
return None
|
|
518
|
+
ret = None
|
|
519
|
+
self.rpc.set_next_node_on_empty_reply(True)
|
|
520
|
+
if self.rpc.get_use_appbase():
|
|
521
|
+
ret = self.rpc.get_hardfork_properties(api="database")
|
|
522
|
+
else:
|
|
523
|
+
ret = self.rpc.get_next_scheduled_hardfork(api="database")
|
|
524
|
+
|
|
525
|
+
return ret
|
|
526
|
+
|
|
527
|
+
def get_network(self, use_stored_data=True, config=None):
|
|
528
|
+
"""Identify the network
|
|
529
|
+
|
|
530
|
+
:param bool use_stored_data: if True, stored data will be returned. If stored data are
|
|
531
|
+
empty or old, refresh_data() is used.
|
|
532
|
+
|
|
533
|
+
:returns: Network parameters
|
|
534
|
+
:rtype: dictionary
|
|
535
|
+
"""
|
|
536
|
+
if use_stored_data:
|
|
537
|
+
self.refresh_data("config")
|
|
538
|
+
return self.data["network"]
|
|
539
|
+
|
|
540
|
+
if self.rpc is None:
|
|
541
|
+
return None
|
|
542
|
+
try:
|
|
543
|
+
return self.rpc.get_network(props=config)
|
|
544
|
+
except:
|
|
545
|
+
return known_chains["HIVE"]
|
|
546
|
+
|
|
547
|
+
def get_median_price(self, use_stored_data=True):
|
|
548
|
+
"""Returns the current median history price as Price"""
|
|
549
|
+
median_price = self.get_current_median_history(use_stored_data=use_stored_data)
|
|
550
|
+
if median_price is None:
|
|
551
|
+
return None
|
|
552
|
+
a = Price(
|
|
553
|
+
None,
|
|
554
|
+
base=Amount(median_price["base"], blockchain_instance=self),
|
|
555
|
+
quote=Amount(median_price["quote"], blockchain_instance=self),
|
|
556
|
+
blockchain_instance=self,
|
|
557
|
+
)
|
|
558
|
+
return a.as_base(self.backed_token_symbol)
|
|
559
|
+
|
|
560
|
+
def get_block_interval(self, use_stored_data=True):
|
|
561
|
+
"""Returns the block interval in seconds"""
|
|
562
|
+
props = self.get_config(use_stored_data=use_stored_data)
|
|
563
|
+
block_interval = 3
|
|
564
|
+
if props is None:
|
|
565
|
+
return block_interval
|
|
566
|
+
for key in props:
|
|
567
|
+
if key[-14:] == "BLOCK_INTERVAL":
|
|
568
|
+
block_interval = props[key]
|
|
569
|
+
|
|
570
|
+
return block_interval
|
|
571
|
+
|
|
572
|
+
def get_blockchain_version(self, use_stored_data=True):
|
|
573
|
+
"""Returns the blockchain version"""
|
|
574
|
+
props = self.get_config(use_stored_data=use_stored_data)
|
|
575
|
+
blockchain_version = "0.0.0"
|
|
576
|
+
if props is None:
|
|
577
|
+
return blockchain_version
|
|
578
|
+
for key in props:
|
|
579
|
+
if key[-18:] == "BLOCKCHAIN_VERSION":
|
|
580
|
+
blockchain_version = props[key]
|
|
581
|
+
return blockchain_version
|
|
582
|
+
|
|
583
|
+
def get_blockchain_name(self, use_stored_data=True):
|
|
584
|
+
"""Returns the blockchain version"""
|
|
585
|
+
props = self.get_config(use_stored_data=use_stored_data)
|
|
586
|
+
blockchain_name = ""
|
|
587
|
+
if props is None:
|
|
588
|
+
return blockchain_name
|
|
589
|
+
for key in props:
|
|
590
|
+
if key[-18:] == "BLOCKCHAIN_VERSION":
|
|
591
|
+
blockchain_name = key.split("_")[0].lower()
|
|
592
|
+
return blockchain_name
|
|
593
|
+
|
|
594
|
+
def get_dust_threshold(self, use_stored_data=True):
|
|
595
|
+
"""Returns the vote dust threshold"""
|
|
596
|
+
props = self.get_config(use_stored_data=use_stored_data)
|
|
597
|
+
dust_threshold = 0
|
|
598
|
+
if props is None:
|
|
599
|
+
return dust_threshold
|
|
600
|
+
for key in props:
|
|
601
|
+
if key[-20:] == "VOTE_DUST_THRESHOLD":
|
|
602
|
+
dust_threshold = props[key]
|
|
603
|
+
return dust_threshold
|
|
604
|
+
|
|
605
|
+
def get_resource_params(self):
|
|
606
|
+
"""Returns the resource parameter"""
|
|
607
|
+
return self.rpc.get_resource_params(api="rc")["resource_params"]
|
|
608
|
+
|
|
609
|
+
def get_resource_pool(self):
|
|
610
|
+
"""Returns the resource pool"""
|
|
611
|
+
return self.rpc.get_resource_pool(api="rc")["resource_pool"]
|
|
612
|
+
|
|
613
|
+
def get_rc_cost(self, resource_count):
|
|
614
|
+
"""Returns the RC costs based on the resource_count"""
|
|
615
|
+
pools = self.get_resource_pool()
|
|
616
|
+
params = self.get_resource_params()
|
|
617
|
+
dyn_param = self.get_dynamic_global_properties()
|
|
618
|
+
rc_regen = int(Amount(dyn_param["total_vesting_shares"], blockchain_instance=self)) / (
|
|
619
|
+
STEEM_RC_REGEN_TIME / self.get_block_interval()
|
|
620
|
+
)
|
|
621
|
+
total_cost = 0
|
|
622
|
+
if rc_regen == 0:
|
|
623
|
+
return total_cost
|
|
624
|
+
for resource_type in resource_count:
|
|
625
|
+
curve_params = params[resource_type]["price_curve_params"]
|
|
626
|
+
current_pool = int(pools[resource_type]["pool"])
|
|
627
|
+
count = resource_count[resource_type]
|
|
628
|
+
count *= params[resource_type]["resource_dynamics_params"]["resource_unit"]
|
|
629
|
+
cost = self._compute_rc_cost(curve_params, current_pool, count, rc_regen)
|
|
630
|
+
total_cost += cost
|
|
631
|
+
return total_cost
|
|
632
|
+
|
|
633
|
+
def _compute_rc_cost(self, curve_params, current_pool, resource_count, rc_regen):
|
|
634
|
+
"""Helper function for computing the RC costs"""
|
|
635
|
+
num = int(rc_regen)
|
|
636
|
+
num *= int(curve_params["coeff_a"])
|
|
637
|
+
num = int(num) >> int(curve_params["shift"])
|
|
638
|
+
num += 1
|
|
639
|
+
num *= int(resource_count)
|
|
640
|
+
denom = int(curve_params["coeff_b"])
|
|
641
|
+
if int(current_pool) > 0:
|
|
642
|
+
denom += int(current_pool)
|
|
643
|
+
num_denom = num / denom
|
|
644
|
+
return int(num_denom) + 1
|
|
645
|
+
|
|
646
|
+
def _max_vote_denom(self, use_stored_data=True):
|
|
647
|
+
# get props
|
|
648
|
+
global_properties = self.get_dynamic_global_properties(use_stored_data=use_stored_data)
|
|
649
|
+
vote_power_reserve_rate = global_properties["vote_power_reserve_rate"]
|
|
650
|
+
max_vote_denom = vote_power_reserve_rate * STEEM_VOTE_REGENERATION_SECONDS
|
|
651
|
+
return max_vote_denom
|
|
652
|
+
|
|
653
|
+
def _calc_resulting_vote(
|
|
654
|
+
self, voting_power=STEEM_100_PERCENT, vote_pct=STEEM_100_PERCENT, use_stored_data=True
|
|
655
|
+
):
|
|
656
|
+
# determine voting power used
|
|
657
|
+
used_power = int((voting_power * abs(vote_pct)) / STEEM_100_PERCENT * (60 * 60 * 24))
|
|
658
|
+
max_vote_denom = self._max_vote_denom(use_stored_data=use_stored_data)
|
|
659
|
+
used_power = int((used_power + max_vote_denom - 1) / max_vote_denom)
|
|
660
|
+
return used_power
|
|
661
|
+
|
|
662
|
+
def _calc_vote_claim(self, effective_vote_rshares, post_rshares):
|
|
663
|
+
post_rshares_normalized = post_rshares + CURVE_CONSTANT
|
|
664
|
+
post_rshares_after_vote_normalized = post_rshares + effective_vote_rshares + CURVE_CONSTANT
|
|
665
|
+
post_rshares_curve = (
|
|
666
|
+
post_rshares_normalized * post_rshares_normalized - SQUARED_CURVE_CONSTANT
|
|
667
|
+
) / (post_rshares + CURVE_CONSTANT_X4)
|
|
668
|
+
post_rshares_curve_after_vote = (
|
|
669
|
+
post_rshares_after_vote_normalized * post_rshares_after_vote_normalized
|
|
670
|
+
- SQUARED_CURVE_CONSTANT
|
|
671
|
+
) / (post_rshares + effective_vote_rshares + CURVE_CONSTANT_X4)
|
|
672
|
+
vote_claim = post_rshares_curve_after_vote - post_rshares_curve
|
|
673
|
+
return vote_claim
|
|
674
|
+
|
|
675
|
+
def _calc_revert_vote_claim(self, vote_claim, post_rshares):
|
|
676
|
+
post_rshares_normalized = post_rshares + CURVE_CONSTANT
|
|
677
|
+
post_rshares_curve = (
|
|
678
|
+
post_rshares_normalized * post_rshares_normalized - SQUARED_CURVE_CONSTANT
|
|
679
|
+
) / (post_rshares + CURVE_CONSTANT_X4)
|
|
680
|
+
post_rshares_curve_after_vote = vote_claim + post_rshares_curve
|
|
681
|
+
|
|
682
|
+
a = 1
|
|
683
|
+
b = -post_rshares_curve_after_vote + 2 * post_rshares_normalized
|
|
684
|
+
c = (
|
|
685
|
+
post_rshares_normalized * post_rshares_normalized - SQUARED_CURVE_CONSTANT
|
|
686
|
+
) - post_rshares_curve_after_vote * (post_rshares + CURVE_CONSTANT_X4)
|
|
687
|
+
# (effective_vote_rshares * effective_vote_rshares) + effective_vote_rshares * (-post_rshares_curve_after_vote + 2 * post_rshares_normalized) + ((post_rshares_normalized * post_rshares_normalized - SQUARED_CURVE_CONSTANT) - post_rshares_curve_after_vote * (post_rshares + CURVE_CONSTANT_X4)) = 0
|
|
688
|
+
|
|
689
|
+
x1 = (-b + math.sqrt(b * b - 4 * a * c)) / (2 * a)
|
|
690
|
+
x2 = (-b - math.sqrt(b * b - 4 * a * c)) / (2 * a)
|
|
691
|
+
if x1 >= 0:
|
|
692
|
+
return x1
|
|
693
|
+
else:
|
|
694
|
+
return x2
|
|
695
|
+
|
|
696
|
+
def vests_to_rshares(
|
|
697
|
+
self,
|
|
698
|
+
vests,
|
|
699
|
+
voting_power=STEEM_100_PERCENT,
|
|
700
|
+
vote_pct=STEEM_100_PERCENT,
|
|
701
|
+
subtract_dust_threshold=True,
|
|
702
|
+
use_stored_data=True,
|
|
703
|
+
):
|
|
704
|
+
"""Obtain the r-shares from vests
|
|
705
|
+
|
|
706
|
+
:param number vests: vesting shares
|
|
707
|
+
:param int voting_power: voting power (100% = 10000)
|
|
708
|
+
:param int vote_pct: voting percentage (100% = 10000)
|
|
709
|
+
|
|
710
|
+
"""
|
|
711
|
+
used_power = self._calc_resulting_vote(
|
|
712
|
+
voting_power=voting_power, vote_pct=vote_pct, use_stored_data=use_stored_data
|
|
713
|
+
)
|
|
714
|
+
# calculate vote rshares
|
|
715
|
+
rshares = int(math.copysign(vests * 1e6 * used_power / STEEM_100_PERCENT, vote_pct))
|
|
716
|
+
if subtract_dust_threshold:
|
|
717
|
+
if abs(rshares) <= self.get_dust_threshold(use_stored_data=use_stored_data):
|
|
718
|
+
return 0
|
|
719
|
+
rshares -= math.copysign(
|
|
720
|
+
self.get_dust_threshold(use_stored_data=use_stored_data), vote_pct
|
|
721
|
+
)
|
|
722
|
+
return rshares
|
|
723
|
+
|
|
724
|
+
def token_power_to_vests(self, token_power, timestamp=None, use_stored_data=True):
|
|
725
|
+
"""Converts TokenPower to vests
|
|
726
|
+
|
|
727
|
+
:param float token_power: Token power to convert
|
|
728
|
+
:param datetime timestamp: (Optional) Can be used to calculate
|
|
729
|
+
the conversion rate from the past
|
|
730
|
+
"""
|
|
731
|
+
raise Exception("not implemented")
|
|
732
|
+
|
|
733
|
+
def vests_to_token_power(self, vests, timestamp=None, use_stored_data=True):
|
|
734
|
+
"""Converts vests to TokenPower
|
|
735
|
+
|
|
736
|
+
:param amount.Amount vests/float vests: Vests to convert
|
|
737
|
+
:param int timestamp: (Optional) Can be used to calculate
|
|
738
|
+
the conversion rate from the past
|
|
739
|
+
|
|
740
|
+
"""
|
|
741
|
+
raise Exception("not implemented")
|
|
742
|
+
|
|
743
|
+
def get_token_per_mvest(self, time_stamp=None, use_stored_data=True):
|
|
744
|
+
"""Returns the MVEST to TOKEN ratio
|
|
745
|
+
|
|
746
|
+
:param int time_stamp: (optional) if set, return an estimated
|
|
747
|
+
TOKEN per MVEST ratio for the given time stamp. If unset the
|
|
748
|
+
current ratio is returned (default). (can also be a datetime object)
|
|
749
|
+
"""
|
|
750
|
+
raise Exception("not implemented")
|
|
751
|
+
|
|
752
|
+
def rshares_to_token_backed_dollar(
|
|
753
|
+
self, rshares, not_broadcasted_vote=False, use_stored_data=True
|
|
754
|
+
):
|
|
755
|
+
"""Calculates the current HBD value of a vote"""
|
|
756
|
+
raise Exception("not implemented")
|
|
757
|
+
|
|
758
|
+
def token_power_to_token_backed_dollar(
|
|
759
|
+
self,
|
|
760
|
+
token_power,
|
|
761
|
+
post_rshares=0,
|
|
762
|
+
voting_power=STEEM_100_PERCENT,
|
|
763
|
+
vote_pct=STEEM_100_PERCENT,
|
|
764
|
+
not_broadcasted_vote=True,
|
|
765
|
+
use_stored_data=True,
|
|
766
|
+
):
|
|
767
|
+
"""Obtain the resulting Token backed dollar vote value from Token power
|
|
768
|
+
|
|
769
|
+
:param number hive_power: Token Power
|
|
770
|
+
:param int post_rshares: rshares of post which is voted
|
|
771
|
+
:param int voting_power: voting power (100% = 10000)
|
|
772
|
+
:param int vote_pct: voting percentage (100% = 10000)
|
|
773
|
+
:param bool not_broadcasted_vote: not_broadcasted or already broadcasted vote (True = not_broadcasted vote).
|
|
774
|
+
|
|
775
|
+
Only impactful for very big votes. Slight modification to the value calculation, as the not_broadcasted
|
|
776
|
+
vote rshares decreases the reward pool.
|
|
777
|
+
"""
|
|
778
|
+
raise Exception("not implemented")
|
|
779
|
+
|
|
780
|
+
def get_chain_properties(self, use_stored_data=True):
|
|
781
|
+
"""Return witness elected chain properties
|
|
782
|
+
|
|
783
|
+
Properties:::
|
|
784
|
+
|
|
785
|
+
{
|
|
786
|
+
'account_creation_fee': '30.000 STEEM',
|
|
787
|
+
'maximum_block_size': 65536,
|
|
788
|
+
'sbd_interest_rate': 250
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
"""
|
|
792
|
+
if use_stored_data:
|
|
793
|
+
self.refresh_data("witness_schedule")
|
|
794
|
+
return self.data["witness_schedule"]["median_props"]
|
|
795
|
+
else:
|
|
796
|
+
return self.get_witness_schedule(use_stored_data)["median_props"]
|
|
797
|
+
|
|
798
|
+
def get_witness_schedule(self, use_stored_data=True):
|
|
799
|
+
"""Return witness elected chain properties"""
|
|
800
|
+
if use_stored_data:
|
|
801
|
+
self.refresh_data("witness_schedule")
|
|
802
|
+
return self.data["witness_schedule"]
|
|
803
|
+
|
|
804
|
+
if self.rpc is None:
|
|
805
|
+
return None
|
|
806
|
+
self.rpc.set_next_node_on_empty_reply(True)
|
|
807
|
+
return self.rpc.get_witness_schedule(api="database")
|
|
808
|
+
|
|
809
|
+
def get_config(self, use_stored_data=True):
|
|
810
|
+
"""Returns internal chain configuration.
|
|
811
|
+
|
|
812
|
+
:param bool use_stored_data: If True, the cached value is returned
|
|
813
|
+
"""
|
|
814
|
+
if use_stored_data:
|
|
815
|
+
self.refresh_data("config")
|
|
816
|
+
config = self.data["config"]
|
|
817
|
+
else:
|
|
818
|
+
if self.rpc is None:
|
|
819
|
+
return None
|
|
820
|
+
self.rpc.set_next_node_on_empty_reply(True)
|
|
821
|
+
config = self.rpc.get_config(api="database")
|
|
822
|
+
return config
|
|
823
|
+
|
|
824
|
+
@property
|
|
825
|
+
def chain_params(self):
|
|
826
|
+
if self.offline or self.rpc is None:
|
|
827
|
+
return known_chains["HIVE"]
|
|
828
|
+
else:
|
|
829
|
+
return self.get_network()
|
|
830
|
+
|
|
831
|
+
@property
|
|
832
|
+
def hardfork(self):
|
|
833
|
+
if self.offline or self.rpc is None:
|
|
834
|
+
versions = known_chains["HIVE"]["min_version"]
|
|
835
|
+
else:
|
|
836
|
+
hf_prop = self.get_hardfork_properties()
|
|
837
|
+
if "current_hardfork_version" in hf_prop:
|
|
838
|
+
versions = hf_prop["current_hardfork_version"]
|
|
839
|
+
else:
|
|
840
|
+
versions = self.get_blockchain_version()
|
|
841
|
+
return int(versions.split(".")[1])
|
|
842
|
+
|
|
843
|
+
@property
|
|
844
|
+
def prefix(self):
|
|
845
|
+
return self.chain_params["prefix"]
|
|
846
|
+
|
|
847
|
+
@property
|
|
848
|
+
def is_hive(self):
|
|
849
|
+
config = self.get_config(use_stored_data=True)
|
|
850
|
+
if config is None:
|
|
851
|
+
return False
|
|
852
|
+
return "HIVE_CHAIN_ID" in config
|
|
853
|
+
|
|
854
|
+
@property
|
|
855
|
+
def is_steem(self):
|
|
856
|
+
config = self.get_config(use_stored_data=True)
|
|
857
|
+
if config is None:
|
|
858
|
+
return False
|
|
859
|
+
return "STEEM_CHAIN_ID" in config
|
|
860
|
+
|
|
861
|
+
def set_default_account(self, account):
|
|
862
|
+
"""Set the default account to be used"""
|
|
863
|
+
Account(account, blockchain_instance=self)
|
|
864
|
+
self.config["default_account"] = account
|
|
865
|
+
|
|
866
|
+
def switch_blockchain(self, blockchain, update_nodes=False):
|
|
867
|
+
"""Switches the connected blockchain. Can be either hive or steem.
|
|
868
|
+
|
|
869
|
+
:param str blockchain: can be "hive" or "steem"
|
|
870
|
+
:param bool update_nodes: When true, the nodes are updated, using
|
|
871
|
+
NodeList.update_nodes()
|
|
872
|
+
"""
|
|
873
|
+
assert blockchain in ["hive", "steem"]
|
|
874
|
+
if blockchain == self.config["default_chain"] and not update_nodes:
|
|
875
|
+
return
|
|
876
|
+
from nectar.nodelist import NodeList
|
|
877
|
+
|
|
878
|
+
nodelist = NodeList()
|
|
879
|
+
if update_nodes:
|
|
880
|
+
nodelist.update_nodes()
|
|
881
|
+
if blockchain == "hive":
|
|
882
|
+
self.set_default_nodes(nodelist.get_hive_nodes())
|
|
883
|
+
else:
|
|
884
|
+
self.set_default_nodes(nodelist.get_steem_nodes())
|
|
885
|
+
self.config["default_chain"] = blockchain
|
|
886
|
+
if not self.offline:
|
|
887
|
+
self.connect(node="")
|
|
888
|
+
|
|
889
|
+
def set_password_storage(self, password_storage):
|
|
890
|
+
"""Set the password storage mode.
|
|
891
|
+
|
|
892
|
+
When set to "no", the password has to be provided each time.
|
|
893
|
+
When set to "environment" the password is taken from the
|
|
894
|
+
UNLOCK variable
|
|
895
|
+
|
|
896
|
+
When set to "keyring" the password is taken from the
|
|
897
|
+
python keyring module. A wallet password can be stored with
|
|
898
|
+
python -m keyring set nectar wallet password
|
|
899
|
+
|
|
900
|
+
:param str password_storage: can be "no",
|
|
901
|
+
"keyring" or "environment"
|
|
902
|
+
|
|
903
|
+
"""
|
|
904
|
+
self.config["password_storage"] = password_storage
|
|
905
|
+
|
|
906
|
+
def set_default_nodes(self, nodes):
|
|
907
|
+
"""Set the default nodes to be used"""
|
|
908
|
+
if bool(nodes):
|
|
909
|
+
if isinstance(nodes, list):
|
|
910
|
+
nodes = str(nodes)
|
|
911
|
+
self.config["node"] = nodes
|
|
912
|
+
else:
|
|
913
|
+
self.config.delete("node")
|
|
914
|
+
|
|
915
|
+
def get_default_nodes(self):
|
|
916
|
+
"""Returns the default nodes"""
|
|
917
|
+
if "node" in self.config:
|
|
918
|
+
nodes = self.config["node"]
|
|
919
|
+
elif "nodes" in self.config:
|
|
920
|
+
nodes = self.config["nodes"]
|
|
921
|
+
elif "node" in self.config.defaults:
|
|
922
|
+
nodes = self.config["node"]
|
|
923
|
+
elif "default_nodes" in self.config and bool(self.config["default_nodes"]):
|
|
924
|
+
nodes = self.config["default_nodes"]
|
|
925
|
+
else:
|
|
926
|
+
nodes = []
|
|
927
|
+
if isinstance(nodes, str) and nodes[0] == "[" and nodes[-1] == "]":
|
|
928
|
+
nodes = ast.literal_eval(nodes)
|
|
929
|
+
return nodes
|
|
930
|
+
|
|
931
|
+
def move_current_node_to_front(self):
|
|
932
|
+
"""Returns the default node list, until the first entry
|
|
933
|
+
is equal to the current working node url
|
|
934
|
+
"""
|
|
935
|
+
node = self.get_default_nodes()
|
|
936
|
+
if len(node) < 2:
|
|
937
|
+
return
|
|
938
|
+
if not isinstance(node, list):
|
|
939
|
+
return
|
|
940
|
+
offline = self.offline
|
|
941
|
+
while not offline and node[0] != self.rpc.url and len(node) > 1:
|
|
942
|
+
node = node[1:] + [node[0]]
|
|
943
|
+
self.set_default_nodes(node)
|
|
944
|
+
|
|
945
|
+
def set_default_vote_weight(self, vote_weight):
|
|
946
|
+
"""Set the default vote weight to be used"""
|
|
947
|
+
self.config["default_vote_weight"] = vote_weight
|
|
948
|
+
|
|
949
|
+
def finalizeOp(self, ops, account, permission, **kwargs):
|
|
950
|
+
"""This method obtains the required private keys if present in
|
|
951
|
+
the wallet, finalizes the transaction, signs it and
|
|
952
|
+
broadacasts it
|
|
953
|
+
|
|
954
|
+
:param ops: The operation (or list of operations) to
|
|
955
|
+
broadcast
|
|
956
|
+
:type ops: list, GrapheneObject
|
|
957
|
+
:param Account account: The account that authorizes the
|
|
958
|
+
operation
|
|
959
|
+
:param string permission: The required permission for
|
|
960
|
+
signing (active, owner, posting)
|
|
961
|
+
:param TransactionBuilder append_to: This allows to provide an instance of
|
|
962
|
+
TransactionBuilder (see :func:`BlockChainInstance.new_tx()`) to specify
|
|
963
|
+
where to put a specific operation.
|
|
964
|
+
|
|
965
|
+
.. note:: ``append_to`` is exposed to every method used in the
|
|
966
|
+
BlockChainInstance class
|
|
967
|
+
|
|
968
|
+
.. note:: If ``ops`` is a list of operation, they all need to be
|
|
969
|
+
signable by the same key! Thus, you cannot combine ops
|
|
970
|
+
that require active permission with ops that require
|
|
971
|
+
posting permission. Neither can you use different
|
|
972
|
+
accounts for different operations!
|
|
973
|
+
|
|
974
|
+
.. note:: This uses :func:`BlockChainInstance.txbuffer` as instance of
|
|
975
|
+
:class:`nectar.transactionbuilder.TransactionBuilder`.
|
|
976
|
+
You may want to use your own txbuffer
|
|
977
|
+
|
|
978
|
+
.. note:: when doing sign + broadcast, the trx_id is added to the returned dict
|
|
979
|
+
|
|
980
|
+
"""
|
|
981
|
+
if self.offline:
|
|
982
|
+
return {}
|
|
983
|
+
if "append_to" in kwargs and kwargs["append_to"]:
|
|
984
|
+
# Append to the append_to and return
|
|
985
|
+
append_to = kwargs["append_to"]
|
|
986
|
+
parent = append_to.get_parent()
|
|
987
|
+
if not isinstance(append_to, (TransactionBuilder)):
|
|
988
|
+
raise AssertionError()
|
|
989
|
+
append_to.appendOps(ops)
|
|
990
|
+
# Add the signer to the buffer so we sign the tx properly
|
|
991
|
+
parent.appendSigner(account, permission)
|
|
992
|
+
# This returns as we used append_to, it does NOT broadcast, or sign
|
|
993
|
+
return append_to.get_parent()
|
|
994
|
+
# Go forward to see what the other options do ...
|
|
995
|
+
else:
|
|
996
|
+
# Append to the default buffer
|
|
997
|
+
self.txbuffer.appendOps(ops)
|
|
998
|
+
|
|
999
|
+
# Add signing information, signer, sign and optionally broadcast
|
|
1000
|
+
if self.unsigned:
|
|
1001
|
+
# In case we don't want to sign anything
|
|
1002
|
+
self.txbuffer.addSigningInformation(account, permission)
|
|
1003
|
+
return self.txbuffer
|
|
1004
|
+
elif self.bundle:
|
|
1005
|
+
# In case we want to add more ops to the tx (bundle)
|
|
1006
|
+
self.txbuffer.appendSigner(account, permission)
|
|
1007
|
+
return self.txbuffer.json()
|
|
1008
|
+
else:
|
|
1009
|
+
# default behavior: sign + broadcast
|
|
1010
|
+
self.txbuffer.appendSigner(account, permission)
|
|
1011
|
+
ret_sign = self.txbuffer.sign()
|
|
1012
|
+
ret = self.txbuffer.broadcast()
|
|
1013
|
+
if ret_sign is not None:
|
|
1014
|
+
ret["trx_id"] = ret_sign.id
|
|
1015
|
+
return ret
|
|
1016
|
+
|
|
1017
|
+
def sign(self, tx=None, wifs=[], reconstruct_tx=True):
|
|
1018
|
+
"""Sign a provided transaction with the provided key(s)
|
|
1019
|
+
|
|
1020
|
+
:param dict tx: The transaction to be signed and returned
|
|
1021
|
+
:param string wifs: One or many wif keys to use for signing
|
|
1022
|
+
a transaction. If not present, the keys will be loaded
|
|
1023
|
+
from the wallet as defined in "missing_signatures" key
|
|
1024
|
+
of the transactions.
|
|
1025
|
+
:param bool reconstruct_tx: when set to False and tx
|
|
1026
|
+
is already contructed, it will not reconstructed
|
|
1027
|
+
and already added signatures remain
|
|
1028
|
+
|
|
1029
|
+
.. note:: The trx_id is added to the returned dict
|
|
1030
|
+
|
|
1031
|
+
"""
|
|
1032
|
+
if tx:
|
|
1033
|
+
txbuffer = TransactionBuilder(tx, blockchain_instance=self)
|
|
1034
|
+
else:
|
|
1035
|
+
txbuffer = self.txbuffer
|
|
1036
|
+
txbuffer.appendWif(wifs)
|
|
1037
|
+
txbuffer.appendMissingSignatures()
|
|
1038
|
+
ret_sign = txbuffer.sign(reconstruct_tx=reconstruct_tx)
|
|
1039
|
+
ret = txbuffer.json()
|
|
1040
|
+
ret["trx_id"] = ret_sign.id
|
|
1041
|
+
return ret
|
|
1042
|
+
|
|
1043
|
+
def broadcast(self, tx=None, trx_id=True):
|
|
1044
|
+
"""Broadcast a transaction to the Hive/Steem network
|
|
1045
|
+
|
|
1046
|
+
:param tx tx: Signed transaction to broadcast
|
|
1047
|
+
:param bool trx_id: when True, the trx_id will be included into the return dict.
|
|
1048
|
+
|
|
1049
|
+
"""
|
|
1050
|
+
if tx:
|
|
1051
|
+
# If tx is provided, we broadcast the tx
|
|
1052
|
+
return TransactionBuilder(tx, blockchain_instance=self).broadcast(trx_id=trx_id)
|
|
1053
|
+
else:
|
|
1054
|
+
return self.txbuffer.broadcast()
|
|
1055
|
+
|
|
1056
|
+
def info(self, use_stored_data=True):
|
|
1057
|
+
"""Returns the global properties"""
|
|
1058
|
+
return self.get_dynamic_global_properties(use_stored_data=use_stored_data)
|
|
1059
|
+
|
|
1060
|
+
# -------------------------------------------------------------------------
|
|
1061
|
+
# Wallet stuff
|
|
1062
|
+
# -------------------------------------------------------------------------
|
|
1063
|
+
def newWallet(self, pwd):
|
|
1064
|
+
"""Create a new wallet. This method is basically only calls
|
|
1065
|
+
:func:`nectar.wallet.Wallet.create`.
|
|
1066
|
+
|
|
1067
|
+
:param str pwd: Password to use for the new wallet
|
|
1068
|
+
|
|
1069
|
+
:raises WalletExists: if there is already a
|
|
1070
|
+
wallet created
|
|
1071
|
+
|
|
1072
|
+
"""
|
|
1073
|
+
return self.wallet.create(pwd)
|
|
1074
|
+
|
|
1075
|
+
def unlock(self, *args, **kwargs):
|
|
1076
|
+
"""Unlock the internal wallet"""
|
|
1077
|
+
return self.wallet.unlock(*args, **kwargs)
|
|
1078
|
+
|
|
1079
|
+
# -------------------------------------------------------------------------
|
|
1080
|
+
# Transaction Buffers
|
|
1081
|
+
# -------------------------------------------------------------------------
|
|
1082
|
+
@property
|
|
1083
|
+
def txbuffer(self):
|
|
1084
|
+
"""Returns the currently active tx buffer"""
|
|
1085
|
+
return self.tx()
|
|
1086
|
+
|
|
1087
|
+
def tx(self):
|
|
1088
|
+
"""Returns the default transaction buffer"""
|
|
1089
|
+
return self._txbuffers[0]
|
|
1090
|
+
|
|
1091
|
+
def new_tx(self, *args, **kwargs):
|
|
1092
|
+
"""Let's obtain a new txbuffer
|
|
1093
|
+
|
|
1094
|
+
:returns: id of the new txbuffer
|
|
1095
|
+
:rtype: int
|
|
1096
|
+
"""
|
|
1097
|
+
builder = TransactionBuilder(*args, blockchain_instance=self, **kwargs)
|
|
1098
|
+
self._txbuffers.append(builder)
|
|
1099
|
+
return builder
|
|
1100
|
+
|
|
1101
|
+
def clear(self):
|
|
1102
|
+
self._txbuffers = []
|
|
1103
|
+
# Base/Default proposal/tx buffers
|
|
1104
|
+
self.new_tx()
|
|
1105
|
+
# self.new_proposal()
|
|
1106
|
+
|
|
1107
|
+
# -------------------------------------------------------------------------
|
|
1108
|
+
# Account related calls
|
|
1109
|
+
# -------------------------------------------------------------------------
|
|
1110
|
+
def claim_account(self, creator, fee=None, **kwargs):
|
|
1111
|
+
"""Claim account for claimed account creation.
|
|
1112
|
+
|
|
1113
|
+
When fee is 0 STEEM/HIVE a subsidized account is claimed and can be created
|
|
1114
|
+
later with create_claimed_account.
|
|
1115
|
+
The number of subsidized account is limited.
|
|
1116
|
+
|
|
1117
|
+
:param str creator: which account should pay the registration fee (RC or STEEM/HIVE)
|
|
1118
|
+
(defaults to ``default_account``)
|
|
1119
|
+
:param str fee: when set to 0 STEEM (default), claim account is paid by RC
|
|
1120
|
+
"""
|
|
1121
|
+
fee = fee if fee is not None else "0 %s" % (self.token_symbol)
|
|
1122
|
+
if not creator and self.config["default_account"]:
|
|
1123
|
+
creator = self.config["default_account"]
|
|
1124
|
+
if not creator:
|
|
1125
|
+
raise ValueError(
|
|
1126
|
+
"Not creator account given. Define it with "
|
|
1127
|
+
+ "creator=x, or set the default_account using hive-nectar"
|
|
1128
|
+
)
|
|
1129
|
+
creator = Account(creator, blockchain_instance=self)
|
|
1130
|
+
op = {
|
|
1131
|
+
"fee": Amount(fee, blockchain_instance=self),
|
|
1132
|
+
"creator": creator["name"],
|
|
1133
|
+
"prefix": self.prefix,
|
|
1134
|
+
"json_str": not bool(self.config["use_condenser"]),
|
|
1135
|
+
}
|
|
1136
|
+
op = operations.Claim_account(**op)
|
|
1137
|
+
return self.finalizeOp(op, creator, "active", **kwargs)
|
|
1138
|
+
|
|
1139
|
+
def create_claimed_account(
|
|
1140
|
+
self,
|
|
1141
|
+
account_name,
|
|
1142
|
+
creator=None,
|
|
1143
|
+
owner_key=None,
|
|
1144
|
+
active_key=None,
|
|
1145
|
+
memo_key=None,
|
|
1146
|
+
posting_key=None,
|
|
1147
|
+
password=None,
|
|
1148
|
+
additional_owner_keys=[],
|
|
1149
|
+
additional_active_keys=[],
|
|
1150
|
+
additional_posting_keys=[],
|
|
1151
|
+
additional_owner_accounts=[],
|
|
1152
|
+
additional_active_accounts=[],
|
|
1153
|
+
additional_posting_accounts=[],
|
|
1154
|
+
storekeys=True,
|
|
1155
|
+
store_owner_key=False,
|
|
1156
|
+
json_meta=None,
|
|
1157
|
+
combine_with_claim_account=False,
|
|
1158
|
+
fee=None,
|
|
1159
|
+
**kwargs,
|
|
1160
|
+
):
|
|
1161
|
+
"""Create new claimed account on Steem
|
|
1162
|
+
|
|
1163
|
+
The brainkey/password can be used to recover all generated keys
|
|
1164
|
+
(see :class:`nectargraphenebase.account` for more details.
|
|
1165
|
+
|
|
1166
|
+
By default, this call will use ``default_account`` to
|
|
1167
|
+
register a new name ``account_name`` with all keys being
|
|
1168
|
+
derived from a new brain key that will be returned. The
|
|
1169
|
+
corresponding keys will automatically be installed in the
|
|
1170
|
+
wallet.
|
|
1171
|
+
|
|
1172
|
+
.. warning:: Don't call this method unless you know what
|
|
1173
|
+
you are doing! Be sure to understand what this
|
|
1174
|
+
method does and where to find the private keys
|
|
1175
|
+
for your account.
|
|
1176
|
+
|
|
1177
|
+
.. note:: Please note that this imports private keys
|
|
1178
|
+
(if password is present) into the wallet by
|
|
1179
|
+
default when nobroadcast is set to False.
|
|
1180
|
+
However, it **does not import the owner
|
|
1181
|
+
key** for security reasons by default.
|
|
1182
|
+
If you set store_owner_key to True, the
|
|
1183
|
+
owner key is stored.
|
|
1184
|
+
Do NOT expect to be able to recover it from
|
|
1185
|
+
the wallet if you lose your password!
|
|
1186
|
+
|
|
1187
|
+
.. note:: Account creations cost a fee that is defined by
|
|
1188
|
+
the network. If you create an account, you will
|
|
1189
|
+
need to pay for that fee!
|
|
1190
|
+
|
|
1191
|
+
:param str account_name: (**required**) new account name
|
|
1192
|
+
:param str json_meta: Optional meta data for the account
|
|
1193
|
+
:param str owner_key: Main owner key
|
|
1194
|
+
:param str active_key: Main active key
|
|
1195
|
+
:param str posting_key: Main posting key
|
|
1196
|
+
:param str memo_key: Main memo_key
|
|
1197
|
+
:param str password: Alternatively to providing keys, one
|
|
1198
|
+
can provide a password from which the
|
|
1199
|
+
keys will be derived
|
|
1200
|
+
:param array additional_owner_keys: Additional owner public keys
|
|
1201
|
+
:param array additional_active_keys: Additional active public keys
|
|
1202
|
+
:param array additional_posting_keys: Additional posting public keys
|
|
1203
|
+
:param array additional_owner_accounts: Additional owner account
|
|
1204
|
+
names
|
|
1205
|
+
:param array additional_active_accounts: Additional acctive account
|
|
1206
|
+
names
|
|
1207
|
+
:param bool storekeys: Store new keys in the wallet (default:
|
|
1208
|
+
``True``)
|
|
1209
|
+
:param bool combine_with_claim_account: When set to True, a
|
|
1210
|
+
claim_account operation is additionally broadcasted
|
|
1211
|
+
:param str fee: When combine_with_claim_account is set to True,
|
|
1212
|
+
this parameter is used for the claim_account operation
|
|
1213
|
+
|
|
1214
|
+
:param str creator: which account should pay the registration fee
|
|
1215
|
+
(defaults to ``default_account``)
|
|
1216
|
+
:raises AccountExistsException: if the account already exists on
|
|
1217
|
+
the blockchain
|
|
1218
|
+
|
|
1219
|
+
"""
|
|
1220
|
+
fee = fee if fee is not None else "0 %s" % (self.token_symbol)
|
|
1221
|
+
if not creator and self.config["default_account"]:
|
|
1222
|
+
creator = self.config["default_account"]
|
|
1223
|
+
if not creator:
|
|
1224
|
+
raise ValueError(
|
|
1225
|
+
"Not creator account given. Define it with "
|
|
1226
|
+
+ "creator=x, or set the default_account using hive-nectar"
|
|
1227
|
+
)
|
|
1228
|
+
if password and (owner_key or active_key or memo_key):
|
|
1229
|
+
raise ValueError("You cannot use 'password' AND provide keys!")
|
|
1230
|
+
|
|
1231
|
+
try:
|
|
1232
|
+
Account(account_name, blockchain_instance=self)
|
|
1233
|
+
raise AccountExistsException
|
|
1234
|
+
except AccountDoesNotExistsException:
|
|
1235
|
+
pass
|
|
1236
|
+
|
|
1237
|
+
creator = Account(creator, blockchain_instance=self)
|
|
1238
|
+
|
|
1239
|
+
" Generate new keys from password"
|
|
1240
|
+
from nectargraphenebase.account import PasswordKey
|
|
1241
|
+
|
|
1242
|
+
if password:
|
|
1243
|
+
active_key = PasswordKey(account_name, password, role="active", prefix=self.prefix)
|
|
1244
|
+
owner_key = PasswordKey(account_name, password, role="owner", prefix=self.prefix)
|
|
1245
|
+
posting_key = PasswordKey(account_name, password, role="posting", prefix=self.prefix)
|
|
1246
|
+
memo_key = PasswordKey(account_name, password, role="memo", prefix=self.prefix)
|
|
1247
|
+
active_pubkey = active_key.get_public_key()
|
|
1248
|
+
owner_pubkey = owner_key.get_public_key()
|
|
1249
|
+
posting_pubkey = posting_key.get_public_key()
|
|
1250
|
+
memo_pubkey = memo_key.get_public_key()
|
|
1251
|
+
active_privkey = active_key.get_private_key()
|
|
1252
|
+
posting_privkey = posting_key.get_private_key()
|
|
1253
|
+
owner_privkey = owner_key.get_private_key()
|
|
1254
|
+
memo_privkey = memo_key.get_private_key()
|
|
1255
|
+
# store private keys
|
|
1256
|
+
try:
|
|
1257
|
+
if storekeys and not self.nobroadcast:
|
|
1258
|
+
if store_owner_key:
|
|
1259
|
+
self.wallet.addPrivateKey(str(owner_privkey))
|
|
1260
|
+
self.wallet.addPrivateKey(str(active_privkey))
|
|
1261
|
+
self.wallet.addPrivateKey(str(memo_privkey))
|
|
1262
|
+
self.wallet.addPrivateKey(str(posting_privkey))
|
|
1263
|
+
except ValueError as e:
|
|
1264
|
+
log.info(str(e))
|
|
1265
|
+
|
|
1266
|
+
elif owner_key and active_key and memo_key and posting_key:
|
|
1267
|
+
active_pubkey = PublicKey(active_key, prefix=self.prefix)
|
|
1268
|
+
owner_pubkey = PublicKey(owner_key, prefix=self.prefix)
|
|
1269
|
+
posting_pubkey = PublicKey(posting_key, prefix=self.prefix)
|
|
1270
|
+
memo_pubkey = PublicKey(memo_key, prefix=self.prefix)
|
|
1271
|
+
else:
|
|
1272
|
+
raise ValueError("Call incomplete! Provide either a password or public keys!")
|
|
1273
|
+
owner = format(owner_pubkey, self.prefix)
|
|
1274
|
+
active = format(active_pubkey, self.prefix)
|
|
1275
|
+
posting = format(posting_pubkey, self.prefix)
|
|
1276
|
+
memo = format(memo_pubkey, self.prefix)
|
|
1277
|
+
|
|
1278
|
+
owner_key_authority = [[owner, 1]]
|
|
1279
|
+
active_key_authority = [[active, 1]]
|
|
1280
|
+
posting_key_authority = [[posting, 1]]
|
|
1281
|
+
owner_accounts_authority = []
|
|
1282
|
+
active_accounts_authority = []
|
|
1283
|
+
posting_accounts_authority = []
|
|
1284
|
+
|
|
1285
|
+
# additional authorities
|
|
1286
|
+
for k in additional_owner_keys:
|
|
1287
|
+
owner_key_authority.append([k, 1])
|
|
1288
|
+
for k in additional_active_keys:
|
|
1289
|
+
active_key_authority.append([k, 1])
|
|
1290
|
+
for k in additional_posting_keys:
|
|
1291
|
+
posting_key_authority.append([k, 1])
|
|
1292
|
+
|
|
1293
|
+
for k in additional_owner_accounts:
|
|
1294
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1295
|
+
owner_accounts_authority.append([addaccount["name"], 1])
|
|
1296
|
+
for k in additional_active_accounts:
|
|
1297
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1298
|
+
active_accounts_authority.append([addaccount["name"], 1])
|
|
1299
|
+
for k in additional_posting_accounts:
|
|
1300
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1301
|
+
posting_accounts_authority.append([addaccount["name"], 1])
|
|
1302
|
+
if combine_with_claim_account:
|
|
1303
|
+
op = {
|
|
1304
|
+
"fee": Amount(fee, blockchain_instance=self),
|
|
1305
|
+
"creator": creator["name"],
|
|
1306
|
+
"prefix": self.prefix,
|
|
1307
|
+
"json_str": not bool(self.config["use_condenser"]),
|
|
1308
|
+
}
|
|
1309
|
+
op = operations.Claim_account(**op)
|
|
1310
|
+
ops = [op]
|
|
1311
|
+
op = {
|
|
1312
|
+
"creator": creator["name"],
|
|
1313
|
+
"new_account_name": account_name,
|
|
1314
|
+
"owner": {
|
|
1315
|
+
"account_auths": owner_accounts_authority,
|
|
1316
|
+
"key_auths": owner_key_authority,
|
|
1317
|
+
"address_auths": [],
|
|
1318
|
+
"weight_threshold": 1,
|
|
1319
|
+
},
|
|
1320
|
+
"active": {
|
|
1321
|
+
"account_auths": active_accounts_authority,
|
|
1322
|
+
"key_auths": active_key_authority,
|
|
1323
|
+
"address_auths": [],
|
|
1324
|
+
"weight_threshold": 1,
|
|
1325
|
+
},
|
|
1326
|
+
"posting": {
|
|
1327
|
+
"account_auths": active_accounts_authority,
|
|
1328
|
+
"key_auths": posting_key_authority,
|
|
1329
|
+
"address_auths": [],
|
|
1330
|
+
"weight_threshold": 1,
|
|
1331
|
+
},
|
|
1332
|
+
"memo_key": memo,
|
|
1333
|
+
"json_metadata": json_meta or {},
|
|
1334
|
+
"prefix": self.prefix,
|
|
1335
|
+
}
|
|
1336
|
+
op = operations.Create_claimed_account(**op)
|
|
1337
|
+
if combine_with_claim_account:
|
|
1338
|
+
ops.append(op)
|
|
1339
|
+
return self.finalizeOp(ops, creator, "active", **kwargs)
|
|
1340
|
+
else:
|
|
1341
|
+
return self.finalizeOp(op, creator, "active", **kwargs)
|
|
1342
|
+
|
|
1343
|
+
def create_account(
|
|
1344
|
+
self,
|
|
1345
|
+
account_name,
|
|
1346
|
+
creator=None,
|
|
1347
|
+
owner_key=None,
|
|
1348
|
+
active_key=None,
|
|
1349
|
+
memo_key=None,
|
|
1350
|
+
posting_key=None,
|
|
1351
|
+
password=None,
|
|
1352
|
+
additional_owner_keys=[],
|
|
1353
|
+
additional_active_keys=[],
|
|
1354
|
+
additional_posting_keys=[],
|
|
1355
|
+
additional_owner_accounts=[],
|
|
1356
|
+
additional_active_accounts=[],
|
|
1357
|
+
additional_posting_accounts=[],
|
|
1358
|
+
storekeys=True,
|
|
1359
|
+
store_owner_key=False,
|
|
1360
|
+
json_meta=None,
|
|
1361
|
+
**kwargs,
|
|
1362
|
+
):
|
|
1363
|
+
"""Create new account on Hive/Steem
|
|
1364
|
+
|
|
1365
|
+
The brainkey/password can be used to recover all generated keys
|
|
1366
|
+
(see :class:`nectargraphenebase.account` for more details.
|
|
1367
|
+
|
|
1368
|
+
By default, this call will use ``default_account`` to
|
|
1369
|
+
register a new name ``account_name`` with all keys being
|
|
1370
|
+
derived from a new brain key that will be returned. The
|
|
1371
|
+
corresponding keys will automatically be installed in the
|
|
1372
|
+
wallet.
|
|
1373
|
+
|
|
1374
|
+
.. warning:: Don't call this method unless you know what
|
|
1375
|
+
you are doing! Be sure to understand what this
|
|
1376
|
+
method does and where to find the private keys
|
|
1377
|
+
for your account.
|
|
1378
|
+
|
|
1379
|
+
.. note:: Please note that this imports private keys
|
|
1380
|
+
(if password is present) into the wallet by
|
|
1381
|
+
default when nobroadcast is set to False.
|
|
1382
|
+
However, it **does not import the owner
|
|
1383
|
+
key** for security reasons by default.
|
|
1384
|
+
If you set store_owner_key to True, the
|
|
1385
|
+
owner key is stored.
|
|
1386
|
+
Do NOT expect to be able to recover it from
|
|
1387
|
+
the wallet if you lose your password!
|
|
1388
|
+
|
|
1389
|
+
.. note:: Account creations cost a fee that is defined by
|
|
1390
|
+
the network. If you create an account, you will
|
|
1391
|
+
need to pay for that fee!
|
|
1392
|
+
|
|
1393
|
+
:param str account_name: (**required**) new account name
|
|
1394
|
+
:param str json_meta: Optional meta data for the account
|
|
1395
|
+
:param str owner_key: Main owner key
|
|
1396
|
+
:param str active_key: Main active key
|
|
1397
|
+
:param str posting_key: Main posting key
|
|
1398
|
+
:param str memo_key: Main memo_key
|
|
1399
|
+
:param str password: Alternatively to providing keys, one
|
|
1400
|
+
can provide a password from which the
|
|
1401
|
+
keys will be derived
|
|
1402
|
+
:param array additional_owner_keys: Additional owner public keys
|
|
1403
|
+
:param array additional_active_keys: Additional active public keys
|
|
1404
|
+
:param array additional_posting_keys: Additional posting public keys
|
|
1405
|
+
:param array additional_owner_accounts: Additional owner account
|
|
1406
|
+
names
|
|
1407
|
+
:param array additional_active_accounts: Additional acctive account
|
|
1408
|
+
names
|
|
1409
|
+
:param bool storekeys: Store new keys in the wallet (default:
|
|
1410
|
+
``True``)
|
|
1411
|
+
|
|
1412
|
+
:param str creator: which account should pay the registration fee
|
|
1413
|
+
(defaults to ``default_account``)
|
|
1414
|
+
:raises AccountExistsException: if the account already exists on
|
|
1415
|
+
the blockchain
|
|
1416
|
+
|
|
1417
|
+
"""
|
|
1418
|
+
if not creator and self.config["default_account"]:
|
|
1419
|
+
creator = self.config["default_account"]
|
|
1420
|
+
if not creator:
|
|
1421
|
+
raise ValueError(
|
|
1422
|
+
"Not creator account given. Define it with "
|
|
1423
|
+
+ "creator=x, or set the default_account using hive-nectar"
|
|
1424
|
+
)
|
|
1425
|
+
if password and (owner_key or active_key or memo_key):
|
|
1426
|
+
raise ValueError("You cannot use 'password' AND provide keys!")
|
|
1427
|
+
|
|
1428
|
+
try:
|
|
1429
|
+
Account(account_name, blockchain_instance=self)
|
|
1430
|
+
raise AccountExistsException
|
|
1431
|
+
except AccountDoesNotExistsException:
|
|
1432
|
+
pass
|
|
1433
|
+
|
|
1434
|
+
creator = Account(creator, blockchain_instance=self)
|
|
1435
|
+
|
|
1436
|
+
" Generate new keys from password"
|
|
1437
|
+
from nectargraphenebase.account import PasswordKey
|
|
1438
|
+
|
|
1439
|
+
if password:
|
|
1440
|
+
active_key = PasswordKey(account_name, password, role="active", prefix=self.prefix)
|
|
1441
|
+
owner_key = PasswordKey(account_name, password, role="owner", prefix=self.prefix)
|
|
1442
|
+
posting_key = PasswordKey(account_name, password, role="posting", prefix=self.prefix)
|
|
1443
|
+
memo_key = PasswordKey(account_name, password, role="memo", prefix=self.prefix)
|
|
1444
|
+
active_pubkey = active_key.get_public_key()
|
|
1445
|
+
owner_pubkey = owner_key.get_public_key()
|
|
1446
|
+
posting_pubkey = posting_key.get_public_key()
|
|
1447
|
+
memo_pubkey = memo_key.get_public_key()
|
|
1448
|
+
active_privkey = active_key.get_private_key()
|
|
1449
|
+
posting_privkey = posting_key.get_private_key()
|
|
1450
|
+
owner_privkey = owner_key.get_private_key()
|
|
1451
|
+
memo_privkey = memo_key.get_private_key()
|
|
1452
|
+
# store private keys
|
|
1453
|
+
try:
|
|
1454
|
+
if storekeys and not self.nobroadcast:
|
|
1455
|
+
if store_owner_key:
|
|
1456
|
+
self.wallet.addPrivateKey(str(owner_privkey))
|
|
1457
|
+
self.wallet.addPrivateKey(str(active_privkey))
|
|
1458
|
+
self.wallet.addPrivateKey(str(memo_privkey))
|
|
1459
|
+
self.wallet.addPrivateKey(str(posting_privkey))
|
|
1460
|
+
except ValueError as e:
|
|
1461
|
+
log.info(str(e))
|
|
1462
|
+
|
|
1463
|
+
elif owner_key and active_key and memo_key and posting_key:
|
|
1464
|
+
active_pubkey = PublicKey(active_key, prefix=self.prefix)
|
|
1465
|
+
owner_pubkey = PublicKey(owner_key, prefix=self.prefix)
|
|
1466
|
+
posting_pubkey = PublicKey(posting_key, prefix=self.prefix)
|
|
1467
|
+
memo_pubkey = PublicKey(memo_key, prefix=self.prefix)
|
|
1468
|
+
else:
|
|
1469
|
+
raise ValueError("Call incomplete! Provide either a password or public keys!")
|
|
1470
|
+
owner = format(owner_pubkey, self.prefix)
|
|
1471
|
+
active = format(active_pubkey, self.prefix)
|
|
1472
|
+
posting = format(posting_pubkey, self.prefix)
|
|
1473
|
+
memo = format(memo_pubkey, self.prefix)
|
|
1474
|
+
|
|
1475
|
+
owner_key_authority = [[owner, 1]]
|
|
1476
|
+
active_key_authority = [[active, 1]]
|
|
1477
|
+
posting_key_authority = [[posting, 1]]
|
|
1478
|
+
owner_accounts_authority = []
|
|
1479
|
+
active_accounts_authority = []
|
|
1480
|
+
posting_accounts_authority = []
|
|
1481
|
+
|
|
1482
|
+
# additional authorities
|
|
1483
|
+
for k in additional_owner_keys:
|
|
1484
|
+
owner_key_authority.append([k, 1])
|
|
1485
|
+
for k in additional_active_keys:
|
|
1486
|
+
active_key_authority.append([k, 1])
|
|
1487
|
+
for k in additional_posting_keys:
|
|
1488
|
+
posting_key_authority.append([k, 1])
|
|
1489
|
+
|
|
1490
|
+
for k in additional_owner_accounts:
|
|
1491
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1492
|
+
owner_accounts_authority.append([addaccount["name"], 1])
|
|
1493
|
+
for k in additional_active_accounts:
|
|
1494
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1495
|
+
active_accounts_authority.append([addaccount["name"], 1])
|
|
1496
|
+
for k in additional_posting_accounts:
|
|
1497
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1498
|
+
posting_accounts_authority.append([addaccount["name"], 1])
|
|
1499
|
+
|
|
1500
|
+
props = self.get_chain_properties()
|
|
1501
|
+
if self.hardfork >= 20:
|
|
1502
|
+
required_fee_steem = Amount(props["account_creation_fee"], blockchain_instance=self)
|
|
1503
|
+
else:
|
|
1504
|
+
required_fee_steem = (
|
|
1505
|
+
Amount(props["account_creation_fee"], blockchain_instance=self) * 30
|
|
1506
|
+
)
|
|
1507
|
+
op = {
|
|
1508
|
+
"fee": required_fee_steem,
|
|
1509
|
+
"creator": creator["name"],
|
|
1510
|
+
"new_account_name": account_name,
|
|
1511
|
+
"owner": {
|
|
1512
|
+
"account_auths": owner_accounts_authority,
|
|
1513
|
+
"key_auths": owner_key_authority,
|
|
1514
|
+
"address_auths": [],
|
|
1515
|
+
"weight_threshold": 1,
|
|
1516
|
+
},
|
|
1517
|
+
"active": {
|
|
1518
|
+
"account_auths": active_accounts_authority,
|
|
1519
|
+
"key_auths": active_key_authority,
|
|
1520
|
+
"address_auths": [],
|
|
1521
|
+
"weight_threshold": 1,
|
|
1522
|
+
},
|
|
1523
|
+
"posting": {
|
|
1524
|
+
"account_auths": posting_accounts_authority,
|
|
1525
|
+
"key_auths": posting_key_authority,
|
|
1526
|
+
"address_auths": [],
|
|
1527
|
+
"weight_threshold": 1,
|
|
1528
|
+
},
|
|
1529
|
+
"memo_key": memo,
|
|
1530
|
+
"json_metadata": json_meta or {},
|
|
1531
|
+
"prefix": self.prefix,
|
|
1532
|
+
"json_str": not bool(self.config["use_condenser"]),
|
|
1533
|
+
}
|
|
1534
|
+
op = operations.Account_create(**op)
|
|
1535
|
+
return self.finalizeOp(op, creator, "active", **kwargs)
|
|
1536
|
+
|
|
1537
|
+
def update_account(
|
|
1538
|
+
self,
|
|
1539
|
+
account,
|
|
1540
|
+
owner_key=None,
|
|
1541
|
+
active_key=None,
|
|
1542
|
+
memo_key=None,
|
|
1543
|
+
posting_key=None,
|
|
1544
|
+
password=None,
|
|
1545
|
+
additional_owner_keys=[],
|
|
1546
|
+
additional_active_keys=[],
|
|
1547
|
+
additional_posting_keys=[],
|
|
1548
|
+
additional_owner_accounts=[],
|
|
1549
|
+
additional_active_accounts=[],
|
|
1550
|
+
additional_posting_accounts=None,
|
|
1551
|
+
storekeys=True,
|
|
1552
|
+
store_owner_key=False,
|
|
1553
|
+
json_meta=None,
|
|
1554
|
+
**kwargs,
|
|
1555
|
+
):
|
|
1556
|
+
"""Update account
|
|
1557
|
+
|
|
1558
|
+
The brainkey/password can be used to recover all generated keys
|
|
1559
|
+
(see :class:`nectargraphenebase.account` for more details.
|
|
1560
|
+
|
|
1561
|
+
The
|
|
1562
|
+
corresponding keys will automatically be installed in the
|
|
1563
|
+
wallet.
|
|
1564
|
+
|
|
1565
|
+
.. warning:: Don't call this method unless you know what
|
|
1566
|
+
you are doing! Be sure to understand what this
|
|
1567
|
+
method does and where to find the private keys
|
|
1568
|
+
for your account.
|
|
1569
|
+
|
|
1570
|
+
.. note:: Please note that this imports private keys
|
|
1571
|
+
(if password is present) into the wallet by
|
|
1572
|
+
default when nobroadcast is set to False.
|
|
1573
|
+
However, it **does not import the owner
|
|
1574
|
+
key** for security reasons by default.
|
|
1575
|
+
If you set store_owner_key to True, the
|
|
1576
|
+
owner key is stored.
|
|
1577
|
+
Do NOT expect to be able to recover it from
|
|
1578
|
+
the wallet if you lose your password!
|
|
1579
|
+
|
|
1580
|
+
:param str account_name: (**required**) account name
|
|
1581
|
+
:param str json_meta: Optional updated meta data for the account
|
|
1582
|
+
:param str owner_key: Main owner (public) key
|
|
1583
|
+
:param str active_key: Main active (public) key
|
|
1584
|
+
:param str posting_key: Main posting (public) key
|
|
1585
|
+
:param str memo_key: Main memo (public) key
|
|
1586
|
+
:param str password: Alternatively to providing keys, one
|
|
1587
|
+
can provide a password from which the
|
|
1588
|
+
keys will be derived
|
|
1589
|
+
:param array additional_owner_keys: Additional owner public keys
|
|
1590
|
+
:param array additional_active_keys: Additional active public keys
|
|
1591
|
+
:param array additional_posting_keys: Additional posting public keys
|
|
1592
|
+
:param array additional_owner_accounts: Additional owner account
|
|
1593
|
+
names
|
|
1594
|
+
:param array additional_active_accounts: Additional acctive account
|
|
1595
|
+
names
|
|
1596
|
+
:param bool storekeys: Store new keys in the wallet (default:
|
|
1597
|
+
``True``)
|
|
1598
|
+
:raises AccountExistsException: if the account already exists on
|
|
1599
|
+
the blockchain
|
|
1600
|
+
|
|
1601
|
+
"""
|
|
1602
|
+
if password and (owner_key or active_key or memo_key):
|
|
1603
|
+
raise ValueError("You cannot use 'password' AND provide keys!")
|
|
1604
|
+
|
|
1605
|
+
account = Account(account, blockchain_instance=self)
|
|
1606
|
+
|
|
1607
|
+
" Generate new keys from password"
|
|
1608
|
+
from nectargraphenebase.account import PasswordKey
|
|
1609
|
+
|
|
1610
|
+
if password:
|
|
1611
|
+
active_key = PasswordKey(account["name"], password, role="active", prefix=self.prefix)
|
|
1612
|
+
owner_key = PasswordKey(account["name"], password, role="owner", prefix=self.prefix)
|
|
1613
|
+
posting_key = PasswordKey(account["name"], password, role="posting", prefix=self.prefix)
|
|
1614
|
+
memo_key = PasswordKey(account["name"], password, role="memo", prefix=self.prefix)
|
|
1615
|
+
active_pubkey = active_key.get_public_key()
|
|
1616
|
+
owner_pubkey = owner_key.get_public_key()
|
|
1617
|
+
posting_pubkey = posting_key.get_public_key()
|
|
1618
|
+
memo_pubkey = memo_key.get_public_key()
|
|
1619
|
+
active_privkey = active_key.get_private_key()
|
|
1620
|
+
posting_privkey = posting_key.get_private_key()
|
|
1621
|
+
owner_privkey = owner_key.get_private_key()
|
|
1622
|
+
memo_privkey = memo_key.get_private_key()
|
|
1623
|
+
# store private keys
|
|
1624
|
+
try:
|
|
1625
|
+
if storekeys and not self.nobroadcast:
|
|
1626
|
+
if store_owner_key:
|
|
1627
|
+
self.wallet.addPrivateKey(str(owner_privkey))
|
|
1628
|
+
self.wallet.addPrivateKey(str(active_privkey))
|
|
1629
|
+
self.wallet.addPrivateKey(str(memo_privkey))
|
|
1630
|
+
self.wallet.addPrivateKey(str(posting_privkey))
|
|
1631
|
+
except ValueError as e:
|
|
1632
|
+
log.info(str(e))
|
|
1633
|
+
|
|
1634
|
+
elif owner_key and active_key and memo_key and posting_key:
|
|
1635
|
+
active_pubkey = PublicKey(active_key, prefix=self.prefix)
|
|
1636
|
+
owner_pubkey = PublicKey(owner_key, prefix=self.prefix)
|
|
1637
|
+
posting_pubkey = PublicKey(posting_key, prefix=self.prefix)
|
|
1638
|
+
memo_pubkey = PublicKey(memo_key, prefix=self.prefix)
|
|
1639
|
+
else:
|
|
1640
|
+
raise ValueError("Call incomplete! Provide either a password or public keys!")
|
|
1641
|
+
owner = format(owner_pubkey, self.prefix)
|
|
1642
|
+
active = format(active_pubkey, self.prefix)
|
|
1643
|
+
posting = format(posting_pubkey, self.prefix)
|
|
1644
|
+
memo = format(memo_pubkey, self.prefix)
|
|
1645
|
+
|
|
1646
|
+
owner_key_authority = [[owner, 1]]
|
|
1647
|
+
active_key_authority = [[active, 1]]
|
|
1648
|
+
posting_key_authority = [[posting, 1]]
|
|
1649
|
+
if additional_owner_accounts is None:
|
|
1650
|
+
owner_accounts_authority = account["owner"]["account_auths"]
|
|
1651
|
+
else:
|
|
1652
|
+
owner_accounts_authority = []
|
|
1653
|
+
if additional_active_accounts is None:
|
|
1654
|
+
active_accounts_authority = account["active"]["account_auths"]
|
|
1655
|
+
else:
|
|
1656
|
+
active_accounts_authority = []
|
|
1657
|
+
if additional_posting_accounts is None:
|
|
1658
|
+
posting_accounts_authority = account["posting"]["account_auths"]
|
|
1659
|
+
else:
|
|
1660
|
+
posting_accounts_authority = []
|
|
1661
|
+
|
|
1662
|
+
# additional authorities
|
|
1663
|
+
for k in additional_owner_keys:
|
|
1664
|
+
owner_key_authority.append([k, 1])
|
|
1665
|
+
for k in additional_active_keys:
|
|
1666
|
+
active_key_authority.append([k, 1])
|
|
1667
|
+
for k in additional_posting_keys:
|
|
1668
|
+
posting_key_authority.append([k, 1])
|
|
1669
|
+
|
|
1670
|
+
if additional_owner_accounts is not None:
|
|
1671
|
+
for k in additional_owner_accounts:
|
|
1672
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1673
|
+
owner_accounts_authority.append([addaccount["name"], 1])
|
|
1674
|
+
if additional_active_accounts is not None:
|
|
1675
|
+
for k in additional_active_accounts:
|
|
1676
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1677
|
+
active_accounts_authority.append([addaccount["name"], 1])
|
|
1678
|
+
if additional_posting_accounts is not None:
|
|
1679
|
+
for k in additional_posting_accounts:
|
|
1680
|
+
addaccount = Account(k, blockchain_instance=self)
|
|
1681
|
+
posting_accounts_authority.append([addaccount["name"], 1])
|
|
1682
|
+
op = {
|
|
1683
|
+
"account": account["name"],
|
|
1684
|
+
"owner": {
|
|
1685
|
+
"account_auths": owner_accounts_authority,
|
|
1686
|
+
"key_auths": owner_key_authority,
|
|
1687
|
+
"address_auths": [],
|
|
1688
|
+
"weight_threshold": 1,
|
|
1689
|
+
},
|
|
1690
|
+
"active": {
|
|
1691
|
+
"account_auths": active_accounts_authority,
|
|
1692
|
+
"key_auths": active_key_authority,
|
|
1693
|
+
"address_auths": [],
|
|
1694
|
+
"weight_threshold": 1,
|
|
1695
|
+
},
|
|
1696
|
+
"posting": {
|
|
1697
|
+
"account_auths": posting_accounts_authority,
|
|
1698
|
+
"key_auths": posting_key_authority,
|
|
1699
|
+
"address_auths": [],
|
|
1700
|
+
"weight_threshold": 1,
|
|
1701
|
+
},
|
|
1702
|
+
"memo_key": memo,
|
|
1703
|
+
"json_metadata": json_meta or account["json_metadata"],
|
|
1704
|
+
"prefix": self.prefix,
|
|
1705
|
+
}
|
|
1706
|
+
op = operations.Account_update(**op)
|
|
1707
|
+
return self.finalizeOp(op, account, "owner", **kwargs)
|
|
1708
|
+
|
|
1709
|
+
def witness_set_properties(self, wif, owner, props):
|
|
1710
|
+
"""Set witness properties
|
|
1711
|
+
|
|
1712
|
+
:param str wif: Private signing key
|
|
1713
|
+
:param dict props: Properties
|
|
1714
|
+
:param str owner: witness account name
|
|
1715
|
+
|
|
1716
|
+
Properties:::
|
|
1717
|
+
|
|
1718
|
+
{
|
|
1719
|
+
"account_creation_fee": x,
|
|
1720
|
+
"account_subsidy_budget": x,
|
|
1721
|
+
"account_subsidy_decay": x,
|
|
1722
|
+
"maximum_block_size": x,
|
|
1723
|
+
"url": x,
|
|
1724
|
+
"sbd_exchange_rate": x,
|
|
1725
|
+
"sbd_interest_rate": x,
|
|
1726
|
+
"new_signing_key": x
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
"""
|
|
1730
|
+
|
|
1731
|
+
owner = Account(owner, blockchain_instance=self)
|
|
1732
|
+
|
|
1733
|
+
try:
|
|
1734
|
+
PrivateKey(wif, prefix=self.prefix)
|
|
1735
|
+
except Exception as e:
|
|
1736
|
+
raise e
|
|
1737
|
+
props_list = [["key", repr(PrivateKey(wif, prefix=self.prefix).pubkey)]]
|
|
1738
|
+
for k in props:
|
|
1739
|
+
props_list.append([k, props[k]])
|
|
1740
|
+
op = operations.Witness_set_properties(
|
|
1741
|
+
{
|
|
1742
|
+
"owner": owner["name"],
|
|
1743
|
+
"props": props_list,
|
|
1744
|
+
"prefix": self.prefix,
|
|
1745
|
+
"json_str": not bool(self.config["use_condenser"]),
|
|
1746
|
+
}
|
|
1747
|
+
)
|
|
1748
|
+
tb = TransactionBuilder(blockchain_instance=self)
|
|
1749
|
+
tb.appendOps([op])
|
|
1750
|
+
tb.appendWif(wif)
|
|
1751
|
+
tb.sign()
|
|
1752
|
+
return tb.broadcast()
|
|
1753
|
+
|
|
1754
|
+
def witness_update(self, signing_key, url, props, account=None, **kwargs):
|
|
1755
|
+
"""Creates/updates a witness
|
|
1756
|
+
|
|
1757
|
+
:param str signing_key: Public signing key
|
|
1758
|
+
:param str url: URL
|
|
1759
|
+
:param dict props: Properties
|
|
1760
|
+
:param str account: (optional) witness account name
|
|
1761
|
+
|
|
1762
|
+
Properties:::
|
|
1763
|
+
|
|
1764
|
+
{
|
|
1765
|
+
"account_creation_fee": "3.000 STEEM",
|
|
1766
|
+
"maximum_block_size": 65536,
|
|
1767
|
+
"sbd_interest_rate": 0,
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
"""
|
|
1771
|
+
if not account and self.config["default_account"]:
|
|
1772
|
+
account = self.config["default_account"]
|
|
1773
|
+
if not account:
|
|
1774
|
+
raise ValueError("You need to provide an account")
|
|
1775
|
+
|
|
1776
|
+
account = Account(account, blockchain_instance=self)
|
|
1777
|
+
|
|
1778
|
+
try:
|
|
1779
|
+
PublicKey(signing_key, prefix=self.prefix)
|
|
1780
|
+
except Exception as e:
|
|
1781
|
+
raise e
|
|
1782
|
+
if "account_creation_fee" in props:
|
|
1783
|
+
props["account_creation_fee"] = Amount(
|
|
1784
|
+
props["account_creation_fee"], blockchain_instance=self
|
|
1785
|
+
)
|
|
1786
|
+
op = operations.Witness_update(
|
|
1787
|
+
**{
|
|
1788
|
+
"owner": account["name"],
|
|
1789
|
+
"url": url,
|
|
1790
|
+
"block_signing_key": signing_key,
|
|
1791
|
+
"props": props,
|
|
1792
|
+
"fee": Amount(0, self.token_symbol, blockchain_instance=self),
|
|
1793
|
+
"prefix": self.prefix,
|
|
1794
|
+
"json_str": not bool(self.config["use_condenser"]),
|
|
1795
|
+
}
|
|
1796
|
+
)
|
|
1797
|
+
return self.finalizeOp(op, account, "active", **kwargs)
|
|
1798
|
+
|
|
1799
|
+
def update_proposal_votes(self, proposal_ids, approve, account=None, **kwargs):
|
|
1800
|
+
"""Update proposal votes
|
|
1801
|
+
|
|
1802
|
+
:param list proposal_ids: list of proposal ids
|
|
1803
|
+
:param bool approve: True/False
|
|
1804
|
+
:param str account: (optional) witness account name
|
|
1805
|
+
|
|
1806
|
+
|
|
1807
|
+
"""
|
|
1808
|
+
if not account and self.config["default_account"]:
|
|
1809
|
+
account = self.config["default_account"]
|
|
1810
|
+
if not account:
|
|
1811
|
+
raise ValueError("You need to provide an account")
|
|
1812
|
+
|
|
1813
|
+
account = Account(account, blockchain_instance=self)
|
|
1814
|
+
if not isinstance(proposal_ids, list):
|
|
1815
|
+
proposal_ids = [proposal_ids]
|
|
1816
|
+
|
|
1817
|
+
op = operations.Update_proposal_votes(
|
|
1818
|
+
**{
|
|
1819
|
+
"voter": account["name"],
|
|
1820
|
+
"proposal_ids": proposal_ids,
|
|
1821
|
+
"approve": approve,
|
|
1822
|
+
"prefix": self.prefix,
|
|
1823
|
+
}
|
|
1824
|
+
)
|
|
1825
|
+
return self.finalizeOp(op, account, "active", **kwargs)
|
|
1826
|
+
|
|
1827
|
+
def _test_weights_treshold(self, authority):
|
|
1828
|
+
"""This method raises an error if the threshold of an authority cannot
|
|
1829
|
+
be reached by the weights.
|
|
1830
|
+
|
|
1831
|
+
:param dict authority: An authority of an account
|
|
1832
|
+
:raises ValueError: if the threshold is set too high
|
|
1833
|
+
"""
|
|
1834
|
+
weights = 0
|
|
1835
|
+
for a in authority["account_auths"]:
|
|
1836
|
+
weights += int(a[1])
|
|
1837
|
+
for a in authority["key_auths"]:
|
|
1838
|
+
weights += int(a[1])
|
|
1839
|
+
if authority["weight_threshold"] > weights:
|
|
1840
|
+
raise ValueError("Threshold too restrictive!")
|
|
1841
|
+
if authority["weight_threshold"] == 0:
|
|
1842
|
+
raise ValueError("Cannot have threshold of 0")
|
|
1843
|
+
|
|
1844
|
+
def custom_json(self, id, json_data, required_auths=[], required_posting_auths=[], **kwargs):
|
|
1845
|
+
"""Create a custom json operation
|
|
1846
|
+
|
|
1847
|
+
:param str id: identifier for the custom json (max length 32 bytes)
|
|
1848
|
+
:param json json_data: the json data to put into the custom_json
|
|
1849
|
+
operation
|
|
1850
|
+
:param list required_auths: (optional) required auths
|
|
1851
|
+
:param list required_posting_auths: (optional) posting auths
|
|
1852
|
+
|
|
1853
|
+
.. note:: While reqired auths and required_posting_auths are both
|
|
1854
|
+
optional, one of the two are needed in order to send the custom
|
|
1855
|
+
json.
|
|
1856
|
+
|
|
1857
|
+
.. code-block:: python
|
|
1858
|
+
|
|
1859
|
+
steem.custom_json("id", "json_data",
|
|
1860
|
+
required_posting_auths=['account'])
|
|
1861
|
+
|
|
1862
|
+
"""
|
|
1863
|
+
account = None
|
|
1864
|
+
if len(required_auths):
|
|
1865
|
+
account = required_auths[0]
|
|
1866
|
+
elif len(required_posting_auths):
|
|
1867
|
+
account = required_posting_auths[0]
|
|
1868
|
+
else:
|
|
1869
|
+
raise Exception("At least one account needs to be specified")
|
|
1870
|
+
account = Account(account, full=False, blockchain_instance=self)
|
|
1871
|
+
op = operations.Custom_json(
|
|
1872
|
+
**{
|
|
1873
|
+
"json": json_data,
|
|
1874
|
+
"required_auths": required_auths,
|
|
1875
|
+
"required_posting_auths": required_posting_auths,
|
|
1876
|
+
"id": id,
|
|
1877
|
+
"prefix": self.prefix,
|
|
1878
|
+
}
|
|
1879
|
+
)
|
|
1880
|
+
if len(required_auths) > 0:
|
|
1881
|
+
return self.finalizeOp(op, account, "active", **kwargs)
|
|
1882
|
+
else:
|
|
1883
|
+
return self.finalizeOp(op, account, "posting", **kwargs)
|
|
1884
|
+
|
|
1885
|
+
def post(
|
|
1886
|
+
self,
|
|
1887
|
+
title,
|
|
1888
|
+
body,
|
|
1889
|
+
author=None,
|
|
1890
|
+
permlink=None,
|
|
1891
|
+
reply_identifier=None,
|
|
1892
|
+
json_metadata=None,
|
|
1893
|
+
comment_options=None,
|
|
1894
|
+
community=None,
|
|
1895
|
+
app=None,
|
|
1896
|
+
tags=None,
|
|
1897
|
+
beneficiaries=None,
|
|
1898
|
+
self_vote=False,
|
|
1899
|
+
parse_body=False,
|
|
1900
|
+
**kwargs,
|
|
1901
|
+
):
|
|
1902
|
+
"""Create a new post.
|
|
1903
|
+
If this post is intended as a reply/comment, `reply_identifier` needs
|
|
1904
|
+
to be set with the identifier of the parent post/comment (eg.
|
|
1905
|
+
`@author/permlink`).
|
|
1906
|
+
Optionally you can also set json_metadata, comment_options and upvote
|
|
1907
|
+
the newly created post as an author.
|
|
1908
|
+
Setting category, tags or community will override the values provided
|
|
1909
|
+
in json_metadata and/or comment_options where appropriate.
|
|
1910
|
+
|
|
1911
|
+
:param str title: Title of the post
|
|
1912
|
+
:param str body: Body of the post/comment
|
|
1913
|
+
:param str author: Account are you posting from
|
|
1914
|
+
:param str permlink: Manually set the permlink (defaults to None).
|
|
1915
|
+
If left empty, it will be derived from title automatically.
|
|
1916
|
+
:param str reply_identifier: Identifier of the parent post/comment (only
|
|
1917
|
+
if this post is a reply/comment).
|
|
1918
|
+
:param json_metadata: JSON meta object that can be attached to
|
|
1919
|
+
the post.
|
|
1920
|
+
:type json_metadata: str, dict
|
|
1921
|
+
:param dict comment_options: JSON options object that can be
|
|
1922
|
+
attached to the post.
|
|
1923
|
+
|
|
1924
|
+
Example::
|
|
1925
|
+
|
|
1926
|
+
comment_options = {
|
|
1927
|
+
'max_accepted_payout': '1000000.000 SBD',
|
|
1928
|
+
'percent_steem_dollars': 10000,
|
|
1929
|
+
'allow_votes': True,
|
|
1930
|
+
'allow_curation_rewards': True,
|
|
1931
|
+
'extensions': [[0, {
|
|
1932
|
+
'beneficiaries': [
|
|
1933
|
+
{'account': 'account1', 'weight': 5000},
|
|
1934
|
+
{'account': 'account2', 'weight': 5000},
|
|
1935
|
+
]}
|
|
1936
|
+
]]
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
:param str community: (Optional) Name of the community we are posting
|
|
1940
|
+
into. This will also override the community specified in
|
|
1941
|
+
`json_metadata` and the category
|
|
1942
|
+
:param str app: (Optional) Name of the app which are used for posting
|
|
1943
|
+
when not set, nectar/<version> is used
|
|
1944
|
+
:param tags: (Optional) A list of tags to go with the
|
|
1945
|
+
post. This will also override the tags specified in
|
|
1946
|
+
`json_metadata`. The first tag will be used as a 'category' when community is not specified. If
|
|
1947
|
+
provided as a string, it should be space separated.
|
|
1948
|
+
:type tags: str, list
|
|
1949
|
+
:param list beneficiaries: (Optional) A list of beneficiaries
|
|
1950
|
+
for posting reward distribution. This argument overrides
|
|
1951
|
+
beneficiaries as specified in `comment_options`.
|
|
1952
|
+
|
|
1953
|
+
For example, if we would like to split rewards between account1 and
|
|
1954
|
+
account2::
|
|
1955
|
+
|
|
1956
|
+
beneficiaries = [
|
|
1957
|
+
{'account': 'account1', 'weight': 5000},
|
|
1958
|
+
{'account': 'account2', 'weight': 5000}
|
|
1959
|
+
]
|
|
1960
|
+
|
|
1961
|
+
:param bool self_vote: (Optional) Upvote the post as author, right after
|
|
1962
|
+
posting.
|
|
1963
|
+
:param bool parse_body: (Optional) When set to True, all mentioned users,
|
|
1964
|
+
used links and images are put into users, links and images array inside
|
|
1965
|
+
json_metadata. This will override provided links, images and users inside
|
|
1966
|
+
json_metadata. Hashtags will added to tags until its length is below five entries.
|
|
1967
|
+
|
|
1968
|
+
"""
|
|
1969
|
+
|
|
1970
|
+
# prepare json_metadata
|
|
1971
|
+
json_metadata = json_metadata or {}
|
|
1972
|
+
if isinstance(json_metadata, str):
|
|
1973
|
+
json_metadata = json.loads(json_metadata)
|
|
1974
|
+
|
|
1975
|
+
# override the community
|
|
1976
|
+
if community:
|
|
1977
|
+
json_metadata.update({"community": community})
|
|
1978
|
+
if app:
|
|
1979
|
+
json_metadata.update({"app": app})
|
|
1980
|
+
elif "app" not in json_metadata:
|
|
1981
|
+
json_metadata.update({"app": "nectar/%s" % (nectar_version)})
|
|
1982
|
+
|
|
1983
|
+
if not author and self.config["default_account"]:
|
|
1984
|
+
author = self.config["default_account"]
|
|
1985
|
+
if not author:
|
|
1986
|
+
raise ValueError("You need to provide an account")
|
|
1987
|
+
account = Account(author, blockchain_instance=self)
|
|
1988
|
+
# deal with the category and tags
|
|
1989
|
+
if isinstance(tags, str):
|
|
1990
|
+
tags = list(set([_f for _f in (re.split(r"[\W_]", tags)) if _f]))
|
|
1991
|
+
|
|
1992
|
+
tags = tags or json_metadata.get("tags", [])
|
|
1993
|
+
|
|
1994
|
+
if parse_body:
|
|
1995
|
+
|
|
1996
|
+
def get_urls(mdstring):
|
|
1997
|
+
urls = re.findall(r'http[s]*://[^\s"><\)\(]+', mdstring)
|
|
1998
|
+
return list(dict.fromkeys(urls))
|
|
1999
|
+
|
|
2000
|
+
def get_users(mdstring):
|
|
2001
|
+
users = []
|
|
2002
|
+
for u in re.findall(
|
|
2003
|
+
r"(^|[^a-zA-Z0-9_!#$%&*@@\/]|(^|[^a-zA-Z0-9_+~.-\/#]))[@@]([a-z][-\.a-z\d]+[a-z\d])",
|
|
2004
|
+
mdstring,
|
|
2005
|
+
):
|
|
2006
|
+
users.append(list(u)[-1])
|
|
2007
|
+
return users
|
|
2008
|
+
|
|
2009
|
+
def get_hashtags(mdstring):
|
|
2010
|
+
hashtags = []
|
|
2011
|
+
for t in re.findall(r"(^|\s)(#[-a-z\d]+)", mdstring):
|
|
2012
|
+
hashtags.append(list(t)[-1])
|
|
2013
|
+
return hashtags
|
|
2014
|
+
|
|
2015
|
+
users = []
|
|
2016
|
+
image = []
|
|
2017
|
+
links = []
|
|
2018
|
+
for url in get_urls(body):
|
|
2019
|
+
img_exts = [".jpg", ".png", ".gif", ".svg", ".jpeg"]
|
|
2020
|
+
if os.path.splitext(url)[1].lower() in img_exts:
|
|
2021
|
+
image.append(url)
|
|
2022
|
+
elif url[:25] == "https://images.hive.blog/":
|
|
2023
|
+
image.append(url)
|
|
2024
|
+
else:
|
|
2025
|
+
links.append(url)
|
|
2026
|
+
users = get_users(body)
|
|
2027
|
+
hashtags = get_hashtags(body)
|
|
2028
|
+
users = list(set(users).difference(set([author])))
|
|
2029
|
+
if len(users) > 0:
|
|
2030
|
+
json_metadata.update({"users": users})
|
|
2031
|
+
if len(image) > 0:
|
|
2032
|
+
json_metadata.update({"image": image})
|
|
2033
|
+
if len(links) > 0:
|
|
2034
|
+
json_metadata.update({"links": links})
|
|
2035
|
+
if len(tags) < 5:
|
|
2036
|
+
for i in range(5 - len(tags)):
|
|
2037
|
+
if len(hashtags) > i:
|
|
2038
|
+
tags.append(hashtags[i])
|
|
2039
|
+
|
|
2040
|
+
if tags:
|
|
2041
|
+
# first tag should be a category
|
|
2042
|
+
if community is None:
|
|
2043
|
+
category = tags[0]
|
|
2044
|
+
else:
|
|
2045
|
+
category = community
|
|
2046
|
+
json_metadata.update({"tags": tags})
|
|
2047
|
+
elif community:
|
|
2048
|
+
category = community
|
|
2049
|
+
else:
|
|
2050
|
+
category = None
|
|
2051
|
+
|
|
2052
|
+
# can't provide a category while replying to a post
|
|
2053
|
+
if reply_identifier and category:
|
|
2054
|
+
category = None
|
|
2055
|
+
|
|
2056
|
+
# deal with replies/categories
|
|
2057
|
+
if reply_identifier:
|
|
2058
|
+
parent_author, parent_permlink = resolve_authorperm(reply_identifier)
|
|
2059
|
+
if not permlink:
|
|
2060
|
+
permlink = derive_permlink(title, parent_permlink)
|
|
2061
|
+
elif category:
|
|
2062
|
+
parent_permlink = sanitize_permlink(category)
|
|
2063
|
+
parent_author = ""
|
|
2064
|
+
if not permlink:
|
|
2065
|
+
permlink = derive_permlink(title)
|
|
2066
|
+
else:
|
|
2067
|
+
parent_author = ""
|
|
2068
|
+
parent_permlink = ""
|
|
2069
|
+
if not permlink:
|
|
2070
|
+
permlink = derive_permlink(title)
|
|
2071
|
+
|
|
2072
|
+
post_op = operations.Comment(
|
|
2073
|
+
**{
|
|
2074
|
+
"parent_author": parent_author.strip(),
|
|
2075
|
+
"parent_permlink": parent_permlink.strip(),
|
|
2076
|
+
"author": account["name"],
|
|
2077
|
+
"permlink": permlink.strip(),
|
|
2078
|
+
"title": title.strip(),
|
|
2079
|
+
"body": body,
|
|
2080
|
+
"json_metadata": json_metadata,
|
|
2081
|
+
}
|
|
2082
|
+
)
|
|
2083
|
+
ops = [post_op]
|
|
2084
|
+
|
|
2085
|
+
# if comment_options are used, add a new op to the transaction
|
|
2086
|
+
if comment_options or beneficiaries:
|
|
2087
|
+
comment_op = self._build_comment_options_op(
|
|
2088
|
+
account["name"], permlink, comment_options, beneficiaries
|
|
2089
|
+
)
|
|
2090
|
+
ops.append(comment_op)
|
|
2091
|
+
|
|
2092
|
+
if self_vote:
|
|
2093
|
+
vote_op = operations.Vote(
|
|
2094
|
+
**{
|
|
2095
|
+
"voter": account["name"],
|
|
2096
|
+
"author": account["name"],
|
|
2097
|
+
"permlink": permlink,
|
|
2098
|
+
"weight": STEEM_100_PERCENT,
|
|
2099
|
+
}
|
|
2100
|
+
)
|
|
2101
|
+
ops.append(vote_op)
|
|
2102
|
+
|
|
2103
|
+
return self.finalizeOp(ops, account, "posting", **kwargs)
|
|
2104
|
+
|
|
2105
|
+
def vote(self, weight, identifier, account=None, **kwargs):
|
|
2106
|
+
"""Vote for a post
|
|
2107
|
+
|
|
2108
|
+
:param float weight: Voting weight. Range: -100.0 - +100.0.
|
|
2109
|
+
:param str identifier: Identifier for the post to vote. Takes the
|
|
2110
|
+
form ``@author/permlink``.
|
|
2111
|
+
:param str account: (optional) Account to use for voting. If
|
|
2112
|
+
``account`` is not defined, the ``default_account`` will be used
|
|
2113
|
+
or a ValueError will be raised
|
|
2114
|
+
|
|
2115
|
+
"""
|
|
2116
|
+
if not account:
|
|
2117
|
+
if "default_account" in self.config:
|
|
2118
|
+
account = self.config["default_account"]
|
|
2119
|
+
if not account:
|
|
2120
|
+
raise ValueError("You need to provide an account")
|
|
2121
|
+
account = Account(account, blockchain_instance=self)
|
|
2122
|
+
|
|
2123
|
+
[post_author, post_permlink] = resolve_authorperm(identifier)
|
|
2124
|
+
|
|
2125
|
+
vote_weight = int(float(weight) * STEEM_1_PERCENT)
|
|
2126
|
+
if vote_weight > STEEM_100_PERCENT:
|
|
2127
|
+
vote_weight = STEEM_100_PERCENT
|
|
2128
|
+
if vote_weight < -STEEM_100_PERCENT:
|
|
2129
|
+
vote_weight = -STEEM_100_PERCENT
|
|
2130
|
+
|
|
2131
|
+
op = operations.Vote(
|
|
2132
|
+
**{
|
|
2133
|
+
"voter": account["name"],
|
|
2134
|
+
"author": post_author,
|
|
2135
|
+
"permlink": post_permlink,
|
|
2136
|
+
"weight": vote_weight,
|
|
2137
|
+
}
|
|
2138
|
+
)
|
|
2139
|
+
|
|
2140
|
+
return self.finalizeOp(op, account, "posting", **kwargs)
|
|
2141
|
+
|
|
2142
|
+
def comment_options(self, options, identifier, beneficiaries=[], account=None, **kwargs):
|
|
2143
|
+
"""Set the comment options
|
|
2144
|
+
|
|
2145
|
+
:param dict options: The options to define.
|
|
2146
|
+
:param str identifier: Post identifier
|
|
2147
|
+
:param list beneficiaries: (optional) list of beneficiaries
|
|
2148
|
+
:param str account: (optional) the account to allow access
|
|
2149
|
+
to (defaults to ``default_account``)
|
|
2150
|
+
|
|
2151
|
+
For the options, you have these defaults:::
|
|
2152
|
+
|
|
2153
|
+
{
|
|
2154
|
+
"author": "",
|
|
2155
|
+
"permlink": "",
|
|
2156
|
+
"max_accepted_payout": "1000000.000 SBD",
|
|
2157
|
+
"percent_steem_dollars": 10000,
|
|
2158
|
+
"allow_votes": True,
|
|
2159
|
+
"allow_curation_rewards": True,
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
"""
|
|
2163
|
+
if not account and self.config["default_account"]:
|
|
2164
|
+
account = self.config["default_account"]
|
|
2165
|
+
if not account:
|
|
2166
|
+
raise ValueError("You need to provide an account")
|
|
2167
|
+
account = Account(account, blockchain_instance=self)
|
|
2168
|
+
author, permlink = resolve_authorperm(identifier)
|
|
2169
|
+
op = self._build_comment_options_op(author, permlink, options, beneficiaries)
|
|
2170
|
+
return self.finalizeOp(op, account, "posting", **kwargs)
|
|
2171
|
+
|
|
2172
|
+
def _build_comment_options_op(self, author, permlink, options, beneficiaries):
|
|
2173
|
+
options = remove_from_dict(
|
|
2174
|
+
options or {},
|
|
2175
|
+
[
|
|
2176
|
+
"max_accepted_payout",
|
|
2177
|
+
"percent_steem_dollars",
|
|
2178
|
+
"percent_hbd",
|
|
2179
|
+
"allow_votes",
|
|
2180
|
+
"allow_curation_rewards",
|
|
2181
|
+
"extensions",
|
|
2182
|
+
],
|
|
2183
|
+
keep_keys=True,
|
|
2184
|
+
)
|
|
2185
|
+
# override beneficiaries extension
|
|
2186
|
+
if beneficiaries:
|
|
2187
|
+
# validate schema
|
|
2188
|
+
# or just simply vo.Schema([{'account': str, 'weight': int}])
|
|
2189
|
+
|
|
2190
|
+
weight_sum = 0
|
|
2191
|
+
for b in beneficiaries:
|
|
2192
|
+
if "account" not in b:
|
|
2193
|
+
raise ValueError("beneficiaries need an account field!")
|
|
2194
|
+
if "weight" not in b:
|
|
2195
|
+
b["weight"] = STEEM_100_PERCENT
|
|
2196
|
+
if len(b["account"]) > 16:
|
|
2197
|
+
raise ValueError("beneficiaries error, account name length >16!")
|
|
2198
|
+
if b["weight"] < 1 or b["weight"] > STEEM_100_PERCENT:
|
|
2199
|
+
raise ValueError("beneficiaries error, 1<=weight<=%s!" % (STEEM_100_PERCENT))
|
|
2200
|
+
weight_sum += b["weight"]
|
|
2201
|
+
|
|
2202
|
+
if weight_sum > STEEM_100_PERCENT:
|
|
2203
|
+
raise ValueError("beneficiaries exceed total weight limit %s" % STEEM_100_PERCENT)
|
|
2204
|
+
|
|
2205
|
+
options["beneficiaries"] = beneficiaries
|
|
2206
|
+
|
|
2207
|
+
default_max_payout = "1000000.000 %s" % (self.backed_token_symbol)
|
|
2208
|
+
if self.is_hive:
|
|
2209
|
+
comment_op = operations.Comment_options(
|
|
2210
|
+
**{
|
|
2211
|
+
"author": author,
|
|
2212
|
+
"permlink": permlink,
|
|
2213
|
+
"max_accepted_payout": options.get("max_accepted_payout", default_max_payout),
|
|
2214
|
+
"percent_hbd": int(options.get("percent_hbd", STEEM_100_PERCENT)),
|
|
2215
|
+
"allow_votes": options.get("allow_votes", True),
|
|
2216
|
+
"allow_curation_rewards": options.get("allow_curation_rewards", True),
|
|
2217
|
+
"extensions": options.get("extensions", []),
|
|
2218
|
+
"beneficiaries": options.get("beneficiaries", []),
|
|
2219
|
+
"prefix": self.prefix,
|
|
2220
|
+
}
|
|
2221
|
+
)
|
|
2222
|
+
else:
|
|
2223
|
+
comment_op = operations.Comment_options(
|
|
2224
|
+
**{
|
|
2225
|
+
"author": author,
|
|
2226
|
+
"permlink": permlink,
|
|
2227
|
+
"max_accepted_payout": options.get("max_accepted_payout", default_max_payout),
|
|
2228
|
+
"percent_steem_dollars": int(
|
|
2229
|
+
options.get("percent_steem_dollars", STEEM_100_PERCENT)
|
|
2230
|
+
),
|
|
2231
|
+
"allow_votes": options.get("allow_votes", True),
|
|
2232
|
+
"allow_curation_rewards": options.get("allow_curation_rewards", True),
|
|
2233
|
+
"extensions": options.get("extensions", []),
|
|
2234
|
+
"beneficiaries": options.get("beneficiaries", []),
|
|
2235
|
+
"prefix": self.prefix,
|
|
2236
|
+
}
|
|
2237
|
+
)
|
|
2238
|
+
return comment_op
|
|
2239
|
+
|
|
2240
|
+
def get_api_methods(self):
|
|
2241
|
+
"""Returns all supported api methods"""
|
|
2242
|
+
return self.rpc.get_methods(api="jsonrpc")
|
|
2243
|
+
|
|
2244
|
+
def get_apis(self):
|
|
2245
|
+
"""Returns all enabled apis"""
|
|
2246
|
+
api_methods = self.get_api_methods()
|
|
2247
|
+
api_list = []
|
|
2248
|
+
for a in api_methods:
|
|
2249
|
+
api = a.split(".")[0]
|
|
2250
|
+
if api not in api_list:
|
|
2251
|
+
api_list.append(api)
|
|
2252
|
+
return api_list
|
|
2253
|
+
|
|
2254
|
+
def _get_asset_symbol(self, asset_id):
|
|
2255
|
+
"""get the asset symbol from an asset id
|
|
2256
|
+
|
|
2257
|
+
:@param int asset_id: 0 -> SBD, 1 -> STEEM, 2 -> VESTS
|
|
2258
|
+
|
|
2259
|
+
"""
|
|
2260
|
+
for asset in self.chain_params["chain_assets"]:
|
|
2261
|
+
if asset["id"] == asset_id:
|
|
2262
|
+
return asset["symbol"]
|
|
2263
|
+
|
|
2264
|
+
raise KeyError("asset ID not found in chain assets")
|
|
2265
|
+
|
|
2266
|
+
@property
|
|
2267
|
+
def backed_token_symbol(self):
|
|
2268
|
+
"""get the current chains symbol for SBD (e.g. "TBD" on testnet)"""
|
|
2269
|
+
# some networks (e.g. whaleshares) do not have SBD
|
|
2270
|
+
try:
|
|
2271
|
+
symbol = self._get_asset_symbol(0)
|
|
2272
|
+
except KeyError:
|
|
2273
|
+
symbol = self._get_asset_symbol(1)
|
|
2274
|
+
return symbol
|
|
2275
|
+
|
|
2276
|
+
@property
|
|
2277
|
+
def token_symbol(self):
|
|
2278
|
+
"""get the current chains symbol for STEEM (e.g. "TESTS" on testnet)"""
|
|
2279
|
+
return self._get_asset_symbol(1)
|
|
2280
|
+
|
|
2281
|
+
@property
|
|
2282
|
+
def vest_token_symbol(self):
|
|
2283
|
+
"""get the current chains symbol for VESTS"""
|
|
2284
|
+
return self._get_asset_symbol(2)
|