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.

Files changed (86) hide show
  1. hive_nectar-0.0.2.dist-info/METADATA +182 -0
  2. hive_nectar-0.0.2.dist-info/RECORD +86 -0
  3. hive_nectar-0.0.2.dist-info/WHEEL +4 -0
  4. hive_nectar-0.0.2.dist-info/entry_points.txt +2 -0
  5. hive_nectar-0.0.2.dist-info/licenses/LICENSE.txt +23 -0
  6. nectar/__init__.py +32 -0
  7. nectar/account.py +4371 -0
  8. nectar/amount.py +475 -0
  9. nectar/asciichart.py +270 -0
  10. nectar/asset.py +82 -0
  11. nectar/block.py +446 -0
  12. nectar/blockchain.py +1178 -0
  13. nectar/blockchaininstance.py +2284 -0
  14. nectar/blockchainobject.py +221 -0
  15. nectar/blurt.py +563 -0
  16. nectar/cli.py +6285 -0
  17. nectar/comment.py +1217 -0
  18. nectar/community.py +513 -0
  19. nectar/constants.py +111 -0
  20. nectar/conveyor.py +309 -0
  21. nectar/discussions.py +1709 -0
  22. nectar/exceptions.py +149 -0
  23. nectar/hive.py +546 -0
  24. nectar/hivesigner.py +420 -0
  25. nectar/imageuploader.py +72 -0
  26. nectar/instance.py +129 -0
  27. nectar/market.py +1013 -0
  28. nectar/memo.py +449 -0
  29. nectar/message.py +357 -0
  30. nectar/nodelist.py +444 -0
  31. nectar/price.py +557 -0
  32. nectar/profile.py +65 -0
  33. nectar/rc.py +308 -0
  34. nectar/snapshot.py +726 -0
  35. nectar/steem.py +582 -0
  36. nectar/storage.py +53 -0
  37. nectar/transactionbuilder.py +622 -0
  38. nectar/utils.py +545 -0
  39. nectar/version.py +2 -0
  40. nectar/vote.py +557 -0
  41. nectar/wallet.py +472 -0
  42. nectar/witness.py +617 -0
  43. nectarapi/__init__.py +11 -0
  44. nectarapi/exceptions.py +123 -0
  45. nectarapi/graphenerpc.py +589 -0
  46. nectarapi/node.py +178 -0
  47. nectarapi/noderpc.py +229 -0
  48. nectarapi/rpcutils.py +97 -0
  49. nectarapi/version.py +2 -0
  50. nectarbase/__init__.py +14 -0
  51. nectarbase/ledgertransactions.py +75 -0
  52. nectarbase/memo.py +243 -0
  53. nectarbase/objects.py +429 -0
  54. nectarbase/objecttypes.py +22 -0
  55. nectarbase/operationids.py +102 -0
  56. nectarbase/operations.py +1297 -0
  57. nectarbase/signedtransactions.py +48 -0
  58. nectarbase/transactions.py +11 -0
  59. nectarbase/version.py +2 -0
  60. nectargrapheneapi/__init__.py +6 -0
  61. nectargraphenebase/__init__.py +27 -0
  62. nectargraphenebase/account.py +846 -0
  63. nectargraphenebase/aes.py +52 -0
  64. nectargraphenebase/base58.py +192 -0
  65. nectargraphenebase/bip32.py +494 -0
  66. nectargraphenebase/bip38.py +134 -0
  67. nectargraphenebase/chains.py +149 -0
  68. nectargraphenebase/dictionary.py +3 -0
  69. nectargraphenebase/ecdsasig.py +326 -0
  70. nectargraphenebase/objects.py +123 -0
  71. nectargraphenebase/objecttypes.py +6 -0
  72. nectargraphenebase/operationids.py +3 -0
  73. nectargraphenebase/operations.py +23 -0
  74. nectargraphenebase/prefix.py +11 -0
  75. nectargraphenebase/py23.py +38 -0
  76. nectargraphenebase/signedtransactions.py +201 -0
  77. nectargraphenebase/types.py +419 -0
  78. nectargraphenebase/unsignedtransactions.py +283 -0
  79. nectargraphenebase/version.py +2 -0
  80. nectarstorage/__init__.py +38 -0
  81. nectarstorage/base.py +306 -0
  82. nectarstorage/exceptions.py +16 -0
  83. nectarstorage/interfaces.py +237 -0
  84. nectarstorage/masterpassword.py +239 -0
  85. nectarstorage/ram.py +30 -0
  86. 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)