From acbb397571ca2ead503a4adabab5259cbd2c1223 Mon Sep 17 00:00:00 2001 From: Holger Nahrstaedt <holger@nahrstaedt.de> Date: Sun, 25 Feb 2018 10:23:32 +0100 Subject: [PATCH] Account: print_info added voting_power, steem_power, get_voting_value_SBD, get_recharge_time, get_followers, get_following, get_bandwidth, interest added The following operations were added: unfollow, follow, update_account_profile, approvewitness, disapprovewitness, update_memo_key, transfer, transfer_to_vesting, convert, transfer_to_savings, transfer_from_savings, transfer_from_savings_cancel, claim_reward_balance, delegate_vesting_shares, withdraw_vesting Comment: The following operations were added: upvote, downvote, vote, edit, reply, comment_options, post, resteem Discussion all possible discussion_by_... functions were added. Market volume24h, orderbook, trades, market_history, accountopenorders fixed or added buy, sell, cancel fixed steem register_api refresh_data added get_dynamic_global_properties, get_feed_history, get_reward_fund, get_current_median_history_price, get_next_scheduled_hardfork, get_hardfork_version, get_network, get_state, get_config moved from blockchain steem_per_mvest, vests_to_sp, sp_to_vests, sp_to_sbd, sp_to_rshares, get_median_price, get_payout_from_rshares custom_json added utils make_patch added witness feed_publish and update added --- .flake8 | 1 + .travis.yml | 4 + beem/account.py | 600 ++++++++++++++++++++++++++++++++++++- beem/amount.py | 2 +- beem/asset.py | 3 - beem/blockchain.py | 63 +--- beem/comment.py | 403 ++++++++++++++++++++++++- beem/discussions.py | 227 ++++++++++++++ beem/exceptions.py | 6 + beem/market.py | 360 ++++++++++------------ beem/steem.py | 360 +++++++++++++++------- beem/transactionbuilder.py | 2 +- beem/utils.py | 46 +++ beem/wallet.py | 23 +- beem/witness.py | 85 ++++++ beembase/operations.py | 2 +- docs/index.rst | 16 +- docs/tutorials.rst | 10 +- setup.py | 2 +- tests/test_steem.py | 19 +- tests/test_utils.py | 12 +- tox.ini | 3 +- 22 files changed, 1830 insertions(+), 419 deletions(-) diff --git a/.flake8 b/.flake8 index 5a532da5..393dc2fc 100644 --- a/.flake8 +++ b/.flake8 @@ -6,6 +6,7 @@ ignore = E129,E501,F401,E722, E122 exclude = .git, + .eggs, __pycache__, docs/conf.py, old, diff --git a/.travis.yml b/.travis.yml index 5f3756b9..a57c0e23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,10 @@ matrix: - pip install flake8 script: - flake8 + - os: linux + python: 3.4 + env: + - TOXENV=py34 - os: linux python: 3.5 env: diff --git a/beem/account.py b/beem/account.py index 63f79b4e..57616ffc 100644 --- a/beem/account.py +++ b/beem/account.py @@ -2,9 +2,13 @@ from beem.instance import shared_steem_instance from .exceptions import AccountDoesNotExistsException from .blockchainobject import BlockchainObject from .utils import formatTimeString -from datetime import datetime +from beem.amount import Amount +from datetime import datetime, timedelta +from beembase import operations +from beembase.account import PrivateKey, PublicKey import json import math +import random class Account(BlockchainObject): @@ -95,6 +99,23 @@ class Account(BlockchainObject): def rep(self): return self.reputation() + def print_info(self, force_refresh=False, return_str=False): + if force_refresh: + self.refresh() + self.steem.refresh_data(True) + ret = self.name + " (" + str(round(self.rep, 3)) + ") " + ret += str(self.voting_power()) + "%, full in " + self.get_recharge_time_str() + ret += " VP = " + str(self.get_voting_value_SBD()) + "$\n" + ret += str(round(self.steem_power(), 2)) + " SP, " + ret += str(self.balances["available"][0]) + ", " + str(self.balances["available"][1]) + bandwidth = self.get_bandwidth() + ret += "\n" + ret += "Remaining Bandwidth: " + str(round(100 - bandwidth["used"] / bandwidth["allocated"] * 100, 2)) + " %" + ret += " (" + str(round(bandwidth["used"] / 1024)) + " kb of " + str(round(bandwidth["allocated"] / 1024 / 1024)) + " mb)" + if return_str: + return ret + print(ret) + def reputation(self, precision=2): rep = int(self['reputation']) if rep == 0: @@ -107,6 +128,78 @@ class Account(BlockchainObject): else: return score + def voting_power(self, precision=2, with_regeneration=True): + if with_regeneration: + diff_in_seconds = (datetime.utcnow() - formatTimeString(self["last_vote_time"])).total_seconds() + regenerated_vp = diff_in_seconds * 10000 / 86400 / 5 / 100 + else: + regenerated_vp = 0 + total_vp = (self["voting_power"] / 100 + regenerated_vp) + if total_vp > 100: + return 100 + if total_vp < 0: + return 0 + if precision is not None: + return round(total_vp, precision) + else: + return total_vp + + def steem_power(self, onlyOwnSP=False): + if onlyOwnSP: + vests = Amount(self["vesting_shares"]) + else: + vests = Amount(self["vesting_shares"]) - Amount(self["delegated_vesting_shares"]) + Amount(self["received_vesting_shares"]) + return self.steem.vests_to_sp(vests) + + def get_voting_value_SBD(self, voting_weight=100, voting_power=None, steem_power=None, precision=2): + + if voting_power is None: + voting_power = self.voting_power() + if steem_power is None: + sp = self.steem_power() + else: + sp = steem_power + + VoteValue = self.steem.sp_to_sbd(sp, voting_power=voting_power * 100, vote_pct=voting_weight * 100) + return round(VoteValue, precision) + + def get_recharge_time_str(self, voting_power_goal=100): + hours = math.floor(self.get_recharge_hours(voting_power_goal=voting_power_goal, precision=3)) + minutes = math.floor(self.get_recharge_reminder_minutes(voting_power_goal=voting_power_goal, precision=0)) + return str(hours) + ":" + str(minutes).zfill(2) + + def get_recharge_hours(self, voting_power_goal=100, precision=2): + missing_vp = voting_power_goal - self.voting_power(precision=10) + if missing_vp < 0: + return 0 + recharge_seconds = missing_vp * 100 * 5 * 86400 / 10000 + return round(missing_vp * recharge_seconds / 60 / 60, precision) + + def get_recharge_reminder_minutes(self, voting_power_goal=100, precision=0): + hours = self.get_recharge_hours(voting_power_goal=voting_power_goal, precision=5) + reminder_in_minuts = (hours - math.floor(hours)) * 60 + return round(reminder_in_minuts, precision) + + def get_followers(self): + return [ + x['follower'] for x in self._get_followers(direction="follower") + ] + + def get_following(self): + return [ + x['following'] for x in self._get_followers(direction="following") + ] + + def _get_followers(self, direction="follower", last_user=""): + if direction == "follower": + followers = self.steem.rpc.get_followers(self.name, last_user, "blog", 100, api='follow') + elif direction == "following": + followers = self.steem.rpc.get_following(self.name, last_user, "blog", 100, api='follow') + if len(followers) >= 100: + followers += self._get_followers( + direction=direction, last_user=followers[-1][direction])[1:] + return followers + @property def available_balances(self): """ List balances of an account. This call returns instances of @@ -181,6 +274,25 @@ class Account(BlockchainObject): return b return Amount(0, symbol) + def interest(self): + """ Caluclate interest for an account + :param str account: Account name to get interest for + """ + last_payment = formatTimeString(self["sbd_last_interest_payment"]) + next_payment = last_payment + timedelta(days=30) + interest_rate = self.steem.get_dynamic_global_properties()[ + "sbd_interest_rate"] / 100 # percent + interest_amount = (interest_rate / 100) * int( + int(self["sbd_seconds"]) / (60 * 60 * 24 * 356)) * 10**-3 + + return { + "interest": interest_amount, + "last_payment": last_payment, + "next_payment": next_payment, + "next_payment_duration": next_payment - datetime.now(), + "interest_rate": interest_rate, + } + @property def is_fully_loaded(self): """ Is this instance fully loaded / e.g. all data available? @@ -192,6 +304,60 @@ class Account(BlockchainObject): self.full = True self.refresh() + def get_bandwidth(self, bandwidth_type=1, account=None, raw_data=False): + """ get_account_bandwidth """ + if account is None: + account = self["name"] + if raw_data: + return self.steem.rpc.get_account_bandwidth(account, bandwidth_type) + else: + global_properties = self.steem.get_dynamic_global_properties() + received_vesting_shares = Amount(self["received_vesting_shares"]).amount + vesting_shares = Amount(self["vesting_shares"]).amount + max_virtual_bandwidth = float(global_properties["max_virtual_bandwidth"]) + total_vesting_shares = Amount(global_properties["total_vesting_shares"]).amount + allocated_bandwidth = (max_virtual_bandwidth * (vesting_shares + received_vesting_shares) / total_vesting_shares) + allocated_bandwidth = round(allocated_bandwidth / 1000000) + + total_seconds = 604800 + date_bandwidth = formatTimeString(self["last_bandwidth_update"]) + seconds_since_last_update = datetime.utcnow() - date_bandwidth + seconds_since_last_update = seconds_since_last_update.total_seconds() + average_bandwidth = float(self["average_bandwidth"]) + used_bandwidth = 0 + if seconds_since_last_update < total_seconds: + used_bandwidth = (((total_seconds - seconds_since_last_update) * average_bandwidth) / total_seconds) + used_bandwidth = round(used_bandwidth / 1000000) + + return {"used": used_bandwidth, + "allocated": allocated_bandwidth} + # print("bandwidth percent used: " + str(100 * used_bandwidth / allocated_bandwidth)) + # print("bandwidth percent remaining: " + str(100 - (100 * used_bandwidth / allocated_bandwidth))) + + def get_owner_history(self, account=None): + """ get_owner_history """ + if account is None: + account = self["name"] + return self.steem.rpc.get_owner_history(account) + + def get_recovery_request(self, account=None): + """ get_recovery_request """ + if account is None: + account = self["name"] + return self.steem.rpc.get_recovery_request(account) + + def get_follow_count(self, account=None): + """ get_follow_count """ + if account is None: + account = self["name"] + return self.steem.rpc.get_follow_count(account, api="follow") + + def verify_account_authority(self, keys, account=None): + """ verify_account_authority """ + if account is None: + account = self["name"] + return self.steem.rpc.verify_account_authority(account, keys) + def history( self, limit=100, only_ops=[], exclude_ops=[] @@ -253,3 +419,435 @@ class Account(BlockchainObject): break if first < _limit: _limit = first - 1 + + def unfollow(self, unfollow, what=["blog"], account=None): + """ Unfollow another account's blog + :param str unfollow: Follow this account + :param list what: List of states to follow + (defaults to ``['blog']``) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + # FIXME: removing 'blog' from the array requires to first read + # the follow.what from the blockchain + return self.follow(unfollow, what=[], account=account) + + def follow(self, follow, what=["blog"], account=None): + """ Follow another account's blog + :param str follow: Follow this account + :param list what: List of states to follow + (defaults to ``['blog']``) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + if not account: + account = self["name"] + if not account: + raise ValueError("You need to provide an account") + + json_body = [ + 'follow', { + 'follower': account, + 'following': follow, + 'what': what + } + ] + return self.steem.custom_json( + id="follow", json=json_body, required_posting_auths=[account]) + + def update_account_profile(self, profile, account=None): + """ Update an account's meta data (json_meta) + :param dict json: The meta data to use (i.e. use Profile() from + account.py) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + if not account: + account = self["name"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + op = operations.Account_update( + **{ + "account": account["name"], + "memo_key": account["memo_key"], + "json_metadata": profile + }) + return self.steem.finalizeOp(op, account["name"], "active") + + # ------------------------------------------------------------------------- + # Approval and Disapproval of witnesses + # ------------------------------------------------------------------------- + def approvewitness(self, witness, account=None, approve=True, **kwargs): + """ Approve a witness + + :param list witnesses: list of Witness name or id + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + if not account: + account = self["name"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + + # if not isinstance(witnesses, (list, set, tuple)): + # witnesses = {witnesses} + + # for witness in witnesses: + # witness = Witness(witness, steem_instance=self) + + op = operations.Account_witness_vote(**{ + "account": account["name"], + "witness": witness, + "approve": approve + }) + return self.steem.finalizeOp(op, account["name"], "active", **kwargs) + + def disapprovewitness(self, witness, account=None, **kwargs): + """ Disapprove a witness + + :param list witnesses: list of Witness name or id + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + return self.approvewitness( + witness=witness, account=account, approve=False) + + def update_memo_key(self, key, account=None, **kwargs): + """ Update an account's memo public key + + This method does **not** add any private keys to your + wallet but merely changes the memo public key. + + :param str key: New memo public key + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + if not account: + account = self + if not account: + raise ValueError("You need to provide an account") + + PublicKey(key, prefix=self.steem.prefix) + + account = Account(account, steem_instance=self.steem) + account["memo_key"] = key + op = operations.Account_update(**{ + "account": account["name"], + "memo_key": account["memo_key"], + "json_metadata": account["json_metadata"] + }) + return self.steem.finalizeOp(op, account["name"], "active", **kwargs) + + # ------------------------------------------------------------------------- + # Simple Transfer + # ------------------------------------------------------------------------- + def transfer(self, to, amount, asset, memo="", account=None, **kwargs): + """ Transfer an asset to another account. + + :param str to: Recipient + :param float amount: Amount to transfer + :param str asset: Asset to transfer + :param str memo: (optional) Memo, may begin with `#` for encrypted + messaging + :param str account: (optional) the source account for the transfer + if not ``default_account`` + """ + from .memo import Memo + if not account: + account = self + if not account: + raise ValueError("You need to provide an account") + + account = Account(account, steem_instance=self.steem) + amount = Amount(amount, asset, steem_instance=self.steem) + to = Account(to, steem_instance=self.steem) + + memoObj = Memo( + from_account=account, + to_account=to, + steem_instance=self.steem + ) + op = operations.Transfer(**{ + "amount": amount, + "to": to["name"], + "memo": memoObj.encrypt(memo), + "from": account["name"], + }) + return self.steem.finalizeOp(op, account, "active", **kwargs) + + def transfer_to_vesting(self, amount, to=None, account=None, **kwargs): + """ Vest STEEM + + :param float amount: Amount to transfer + :param str to: Recipient (optional) if not set equal to account + :param str account: (optional) the source account for the transfer + if not ``default_account`` + """ + if not account: + account = self + if not account: + raise ValueError("You need to provide an account") + if not to: + to = account # powerup on the same account + account = Account(account, steem_instance=self.steem) + if isinstance(amount, str): + amount = Amount(amount, steem_instance=self.steem) + elif isinstance(amount, Amount): + amount = Amount(amount, steem_instance=self.steem) + else: + amount = Amount(amount, "STEEM", steem_instance=self.steem) + assert amount["symbol"] == "STEEM" + to = Account(to, steem_instance=self.steem) + + op = operations.Transfer_to_vesting(**{ + "from": account["name"], + "to": to["name"], + "amount": amount, + }) + return self.steem.finalizeOp(op, account, "active", **kwargs) + + def convert(self, amount, account=None, request_id=None): + """ Convert SteemDollars to Steem (takes one week to settle) + :param float amount: number of VESTS to withdraw + :param str account: (optional) the source account for the transfer + if not ``default_account`` + :param str request_id: (optional) identifier for tracking the + conversion` + """ + if not account: + account = self + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + if isinstance(amount, str): + amount = Amount(amount, steem_instance=self.steem) + elif isinstance(amount, Amount): + amount = Amount(amount, steem_instance=self.steem) + else: + amount = Amount(amount, "SBD", steem_instance=self.steem) + assert amount["symbol"] == "SBD" + if request_id: + request_id = int(request_id) + else: + request_id = random.getrandbits(32) + op = operations.Convert( + **{ + "owner": account["name"], + "requestid": request_id, + "amount": amount + }) + + return self.steem.finalizeOp(op, account, "active") + + def transfer_to_savings(self, amount, asset, memo, to=None, account=None): + """ Transfer SBD or STEEM into a 'savings' account. + :param float amount: STEEM or SBD amount + :param float asset: 'STEEM' or 'SBD' + :param str memo: (optional) Memo + :param str to: (optional) the source account for the transfer if + not ``default_account`` + :param str account: (optional) the source account for the transfer + if not ``default_account`` + """ + assert asset in ['STEEM', 'SBD'] + + if not account: + account = self + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + amount = Amount(amount, asset, steem_instance=self.steem) + if not to: + to = account # move to savings on same account + + op = operations.Transfer_to_savings( + **{ + "from": account["name"], + "to": to["name"], + "amount": amount, + "memo": memo, + }) + return self.steem.finalizeOp(op, account, "active") + + def transfer_from_savings(self, + amount, + asset, + memo, + request_id=None, + to=None, + account=None): + """ Withdraw SBD or STEEM from 'savings' account. + :param float amount: STEEM or SBD amount + :param float asset: 'STEEM' or 'SBD' + :param str memo: (optional) Memo + :param str request_id: (optional) identifier for tracking or + cancelling the withdrawal + :param str to: (optional) the source account for the transfer if + not ``default_account`` + :param str account: (optional) the source account for the transfer + if not ``default_account`` + """ + assert asset in ['STEEM', 'SBD'] + + if not account: + account = self + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + if not to: + to = account # move to savings on same account + amount = Amount(amount, asset, steem_instance=self.steem) + if request_id: + request_id = int(request_id) + else: + request_id = random.getrandbits(32) + + op = operations.Transfer_from_savings( + **{ + "from": account["name"], + "request_id": request_id, + "to": to["name"], + "amount": amount, + "memo": memo, + }) + return self.steem.finalizeOp(op, account, "active") + + def transfer_from_savings_cancel(self, request_id, account=None): + """ Cancel a withdrawal from 'savings' account. + :param str request_id: Identifier for tracking or cancelling + the withdrawal + :param str account: (optional) the source account for the transfer + if not ``default_account`` + """ + if not account: + account = self + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + op = operations.Cancel_transfer_from_savings(**{ + "from": account["name"], + "request_id": request_id, + }) + return self.steem.finalizeOp(op, account, "active") + + def claim_reward_balance(self, + reward_steem='0 STEEM', + reward_sbd='0 SBD', + reward_vests='0 VESTS', + account=None): + """ Claim reward balances. + By default, this will claim ``all`` outstanding balances. To bypass + this behaviour, set desired claim amount by setting any of + `reward_steem`, `reward_sbd` or `reward_vests`. + Args: + reward_steem (string): Amount of STEEM you would like to claim. + reward_sbd (string): Amount of SBD you would like to claim. + reward_vests (string): Amount of VESTS you would like to claim. + account (string): The source account for the claim if not + ``default_account`` is used. + """ + if not account: + account = self + else: + account = Account(account, steem_instance=self.steem) + if not account: + raise ValueError("You need to provide an account") + + # if no values were set by user, claim all outstanding balances on + # account + if isinstance(reward_steem, str): + reward_steem = Amount(reward_steem, steem_instance=self.steem) + elif isinstance(reward_steem, Amount): + reward_steem = Amount(reward_steem, steem_instance=self.steem) + else: + reward_steem = Amount(reward_steem, "STEEM", steem_instance=self.steem) + assert reward_steem["symbol"] == "STEEM" + + if isinstance(reward_sbd, str): + reward_sbd = Amount(reward_sbd, steem_instance=self.steem) + elif isinstance(reward_sbd, Amount): + reward_sbd = Amount(reward_sbd, steem_instance=self.steem) + else: + reward_sbd = Amount(reward_sbd, "SBD", steem_instance=self.steem) + assert reward_sbd["symbol"] == "SBD" + + if isinstance(reward_vests, str): + reward_vests = Amount(reward_vests, steem_instance=self.steem) + elif isinstance(reward_vests, Amount): + reward_vests = Amount(reward_vests, steem_instance=self.steem) + else: + reward_vests = Amount(reward_vests, "VESTS", steem_instance=self.steem) + assert reward_vests["symbol"] == "VESTS" + if reward_steem.amount == 0 and reward_sbd.amount == 0 and reward_vests.amount == 0: + reward_steem = account.balances["rewards"][0] + reward_sbd = account.balances["rewards"][1] + reward_vests = account.balances["rewards"][2] + + op = operations.Claim_reward_balance( + **{ + "account": account["name"], + "reward_steem": reward_steem, + "reward_sbd": reward_sbd, + "reward_vests": reward_vests, + }) + return self.steem.finalizeOp(op, account, "posting") + + def delegate_vesting_shares(self, to_account, vesting_shares, + account=None): + """ Delegate SP to another account. + Args: + to_account (string): Account we are delegating shares to + (delegatee). + vesting_shares (string): Amount of VESTS to delegate eg. `10000 + VESTS`. + account (string): The source account (delegator). If not specified, + ``default_account`` is used. + """ + if not account: + account = self["name"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + if isinstance(vesting_shares, str): + vesting_shares = Amount(vesting_shares, steem_instance=self.steem) + elif isinstance(vesting_shares, Amount): + vesting_shares = Amount(vesting_shares, steem_instance=self.steem) + else: + vesting_shares = Amount(vesting_shares, "VESTS", steem_instance=self.steem) + assert vesting_shares["symbol"] == "VESTS" + op = operations.Delegate_vesting_shares( + **{ + "delegator": account, + "delegatee": to_account, + "vesting_shares": vesting_shares, + }) + return self.steem.finalizeOp(op, account, "active") + + def withdraw_vesting(self, amount, account=None): + """ Withdraw VESTS from the vesting account. + :param float amount: number of VESTS to withdraw over a period of + 104 weeks + :param str account: (optional) the source account for the transfer + if not ``default_account`` + """ + if not account: + account = self["name"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + if isinstance(amount, str): + amount = Amount(amount, steem_instance=self.steem) + elif isinstance(amount, Amount): + amount = Amount(amount, steem_instance=self.steem) + else: + amount = Amount(amount, "VESTS", steem_instance=self.steem) + assert amount["symbol"] == "VESTS" + op = operations.WithdrawVesting( + **{ + "account": account["name"], + "vesting_shares": amount, + }) + + return self.steem.finalizeOp(op, account, "active") diff --git a/beem/amount.py b/beem/amount.py index 8bc950e3..054a0348 100644 --- a/beem/amount.py +++ b/beem/amount.py @@ -1,5 +1,5 @@ from beem.instance import shared_steem_instance -from .asset import Asset +from beem.asset import Asset class Amount(dict): diff --git a/beem/asset.py b/beem/asset.py index b1f37ec5..09ab2d56 100644 --- a/beem/asset.py +++ b/beem/asset.py @@ -1,7 +1,4 @@ import json -from beem.account import Account -from beembase import operations - from .exceptions import AssetDoesNotExistsException from .blockchainobject import BlockchainObject diff --git a/beem/blockchain.py b/beem/blockchain.py index 8dc0b382..897b8a6c 100644 --- a/beem/blockchain.py +++ b/beem/blockchain.py @@ -2,6 +2,8 @@ import time from .block import Block from beem.instance import shared_steem_instance from beembase.operationids import getOperationNameForId +from .amount import Amount +from datetime import datetime class Blockchain(object): @@ -39,7 +41,8 @@ class Blockchain(object): def __init__( self, steem_instance=None, - mode="irreversible" + mode="irreversible", + data_refresh_time_seconds=900, ): self.steem = steem_instance or shared_steem_instance() @@ -50,67 +53,13 @@ class Blockchain(object): else: raise ValueError("invalid value for 'mode'!") - def get_dynamic_global_properties(self): - """ This call returns the *dynamic global properties* - """ - return self.steem.rpc.get_dynamic_global_properties() - - def get_feed_history(self): - """ Returns the feed_history - """ - return self.steem.rpc.get_feed_history() - - def get_current_median_history_price(self): - """ Returns the current median price - """ - return self.steem.rpc.get_current_median_history_price() - - def get_next_scheduled_hardfork(self): - """ Returns Hardfork and live_time of the hardfork - """ - return self.steem.rpc.get_next_scheduled_hardfork() - - def get_hardfork_version(self): - """ Current Hardfork Version as String - """ - return self.steem.rpc.get_hardfork_version() - - def get_network(self): - """ Identify the network - - :returns: Network parameters - :rtype: dict - """ - return self.steem.rpc.get_network() - - def get_chain_properties(self): - """ Return witness elected chain properties - - :: - {'account_creation_fee': '30.000 STEEM', - 'maximum_block_size': 65536, - 'sbd_interest_rate': 250} - - """ - return self.steem.rpc.get_chain_properties() - - def get_state(self, path="value"): - """ get_state - """ - return self.steem.rpc.get_state(path) - - def get_config(self): - """ Returns internal chain configuration. - """ - return self.steem.rpc.get_config() - def get_current_block_num(self): """ This call returns the current block .. note:: The block number returned depends on the ``mode`` used when instanciating from this class. """ - return self.get_dynamic_global_properties().get(self.mode) + return self.steem.get_dynamic_global_properties(False).get(self.mode) def get_current_block(self): """ This call returns the current block @@ -155,7 +104,7 @@ class Blockchain(object): confirmed by 2/3 of all block producers and is thus irreversible) """ # Let's find out how often blocks are generated! - block_interval = self.get_config().get("STEEMIT_BLOCK_INTERVAL") + block_interval = self.steem.get_config().get("STEEMIT_BLOCK_INTERVAL") if not start: start = self.get_current_block_num() diff --git a/beem/comment.py b/beem/comment.py index 419e2a51..8f8db858 100644 --- a/beem/comment.py +++ b/beem/comment.py @@ -1,10 +1,13 @@ from .instance import shared_steem_instance from .account import Account -from .utils import resolve_authorperm, construct_authorperm +from .utils import resolve_authorperm, construct_authorperm, derive_permlink, keep_in_dict, make_patch from .blockchainobject import BlockchainObject -from .exceptions import ContentDoesNotExistsException +from .exceptions import ContentDoesNotExistsException, VotingInvalidOnArchivedPost +from beembase import operations import json +import re import logging +import difflib log = logging.getLogger(__name__) @@ -25,7 +28,7 @@ class Comment(BlockchainObject): steem_instance=None ): self.full = full - if isinstance(authorperm, str): + if isinstance(authorperm, str) and authorperm != "": [author, permlink] = resolve_authorperm(authorperm) self["author"] = author self["permlink"] = permlink @@ -33,7 +36,7 @@ class Comment(BlockchainObject): elif isinstance(authorperm, dict) and "author" in authorperm and "permlink" in authorperm: self["author"] = authorperm["author"] self["permlink"] = authorperm["permlink"] - self["authorperm"] = construct_authorperm(authorperm) + self["authorperm"] = construct_authorperm(authorperm["author"], authorperm["permlink"]) super().__init__( authorperm, id_item="authorperm", @@ -41,19 +44,20 @@ class Comment(BlockchainObject): full=full, steem_instance=steem_instance ) + self.identifier = self["authorperm"] def refresh(self): - [author, permlink] = resolve_authorperm(self.identifier) - content = self.steem.rpc.get_content(author, permlink) + content = self.steem.rpc.get_content(self["author"], self["permlink"]) if not content: raise ContentDoesNotExistsException super(Comment, self).__init__(content, id_item="authorperm", steem_instance=self.steem) - - self.identifier = self.authorperm + self["authorperm"] = construct_authorperm(self["author"], self["permlink"]) + self.identifier = self["authorperm"] def json(self): output = self - output.pop("authorperm") + if "authorperm" in output: + output.pop("authorperm") return json.loads(str(json.dumps(output))) @property @@ -106,6 +110,387 @@ class Comment(BlockchainObject): """ return self['depth'] > 0 + def upvote(self, weight=+100, voter=None): + """ Upvote the post + :param float weight: (optional) Weight for posting (-100.0 - + +100.0) defaults to +100.0 + :param str voter: (optional) Voting account + """ + if self.get('net_rshares', None) is None: + raise VotingInvalidOnArchivedPost + return self.vote(weight, voter=voter) + + def downvote(self, weight=-100, voter=None): + """ Downvote the post + :param float weight: (optional) Weight for posting (-100.0 - + +100.0) defaults to -100.0 + :param str voter: (optional) Voting account + """ + if self.get('net_rshares', None) is None: + raise VotingInvalidOnArchivedPost + return self.vote(weight, voter=voter) + + def vote(self, weight, account=None, identifier=None, **kwargs): + """ Vote for a post + :param str identifier: Identifier for the post to upvote Takes + the form ``@author/permlink`` + :param float weight: Voting weight. Range: -100.0 - +100.0. May + not be 0.0 + :param str account: Voter to use for voting. (Optional) + If ``voter`` is not defines, the ``default_account`` will be taken + or a ValueError will be raised + """ + if not account: + if "default_account" in self.steem.config: + account = self.steem.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self) + if not identifier: + post_author = self["author"] + post_permlink = self["permlink"] + else: + [post_author, post_permlink] = resolve_authorperm(identifier) + + STEEMIT_100_PERCENT = 10000 + STEEMIT_1_PERCENT = (STEEMIT_100_PERCENT / 100) + vote_weight = int(weight * STEEMIT_1_PERCENT) + if vote_weight > STEEMIT_100_PERCENT: + vote_weight = STEEMIT_100_PERCENT + if vote_weight < STEEMIT_100_PERCENT: + vote_weight = -STEEMIT_100_PERCENT + + op = operations.Vote( + **{ + "voter": account["name"], + "author": post_author, + "permlink": post_permlink, + "weight": int(weight * STEEMIT_1_PERCENT) + }) + + return self.steem.finalizeOp(op, account, "posting", **kwargs) + + def edit(self, body, meta=None, replace=False): + """ Edit an existing post + :param str body: Body of the reply + :param json meta: JSON meta object that can be attached to the + post. (optional) + :param bool replace: Instead of calculating a *diff*, replace + the post entirely (defaults to ``False``) + """ + if not meta: + meta = {} + original_post = self + + if replace: + newbody = body + else: + newbody = make_patch(original_post["body"], body) + if not newbody: + log.info("No changes made! Skipping ...") + return + + reply_identifier = construct_authorperm( + original_post["parent_author"], original_post["parent_permlink"]) + + new_meta = {} + if meta: + if original_post["json_metadata"]: + import json + new_meta = original_post["json_metadata"].update(meta) + else: + new_meta = meta + + return self.post( + original_post["title"], + newbody, + reply_identifier=reply_identifier, + author=original_post["author"], + permlink=original_post["permlink"], + json_metadata=new_meta, + ) + + def reply(self, body, title="", author="", meta=None): + """ Reply to an existing post + :param str body: Body of the reply + :param str title: Title of the reply post + :param str author: Author of reply (optional) if not provided + ``default_user`` will be used, if present, else + a ``ValueError`` will be raised. + :param json meta: JSON meta object that can be attached to the + post. (optional) + """ + return self.post( + title, + body, + json_metadata=meta, + author=author, + reply_identifier=self.identifier) + + def post(self, + title=None, + body=None, + author=None, + permlink=None, + reply_identifier=None, + json_metadata=None, + comment_options=None, + community=None, + tags=None, + beneficiaries=None, + self_vote=False): + """ Create a new post. + If this post is intended as a reply/comment, `reply_identifier` needs + to be set with the identifier of the parent post/comment (eg. + `@author/permlink`). + Optionally you can also set json_metadata, comment_options and upvote + the newly created post as an author. + Setting category, tags or community will override the values provided + in json_metadata and/or comment_options where appropriate. + Args: + title (str): Title of the post + body (str): Body of the post/comment + author (str): Account are you posting from + permlink (str): Manually set the permlink (defaults to None). + If left empty, it will be derived from title automatically. + reply_identifier (str): Identifier of the parent post/comment (only + if this post is a reply/comment). + json_metadata (str, dict): JSON meta object that can be attached to + the post. + comment_options (str, dict): JSON options object that can be + attached to the post. + Example:: + comment_options = { + 'max_accepted_payout': '1000000.000 SBD', + 'percent_steem_dollars': 10000, + 'allow_votes': True, + 'allow_curation_rewards': True, + 'extensions': [[0, { + 'beneficiaries': [ + {'account': 'account1', 'weight': 5000}, + {'account': 'account2', 'weight': 5000}, + ]} + ]] + } + community (str): (Optional) Name of the community we are posting + into. This will also override the community specified in + `json_metadata`. + tags (str, list): (Optional) A list of tags (5 max) to go with the + post. This will also override the tags specified in + `json_metadata`. The first tag will be used as a 'category'. If + provided as a string, it should be space separated. + beneficiaries (list of dicts): (Optional) A list of beneficiaries + for posting reward distribution. This argument overrides + beneficiaries as specified in `comment_options`. + For example, if we would like to split rewards between account1 and + account2:: + beneficiaries = [ + {'account': 'account1', 'weight': 5000}, + {'account': 'account2', 'weight': 5000} + ] + self_vote (bool): (Optional) Upvote the post as author, right after + posting. + """ + + # prepare json_metadata + json_metadata = json_metadata or {} + if isinstance(json_metadata, str): + json_metadata = json.loads(json_metadata) + + # override the community + if community: + json_metadata.update({'community': community}) + + if title is None: + title = self["title"] + if body is None: + body = self["body"] + if author is None and permlink is None: + [author, permlink] = resolve_authorperm(self.identifier) + else: + if author is None: + author = self["author"] + if permlink is None: + permlink = self["permlink"] + account = Account(author, steem_instance=self.steem) + # deal with the category and tags + if isinstance(tags, str): + tags = list(set(filter(None, (re.split("[\W_]", tags))))) + + category = None + tags = tags or json_metadata.get('tags', []) + if tags: + if len(tags) > 5: + raise ValueError('Can only specify up to 5 tags per post.') + + # first tag should be a category + category = tags[0] + json_metadata.update({"tags": tags}) + + # can't provide a category while replying to a post + if reply_identifier and category: + category = None + + # deal with replies/categories + if reply_identifier: + parent_author, parent_permlink = resolve_authorperm( + reply_identifier) + if not permlink: + permlink = derive_permlink(title, parent_permlink) + elif category: + parent_permlink = derive_permlink(category) + parent_author = "" + if not permlink: + permlink = derive_permlink(title) + else: + parent_author = "" + parent_permlink = "" + if not permlink: + permlink = derive_permlink(title) + + post_op = operations.Comment( + **{ + "parent_author": parent_author, + "parent_permlink": parent_permlink, + "author": author, + "permlink": permlink, + "title": title, + "body": body, + "json_metadata": json_metadata + }) + ops = [post_op] + + # if comment_options are used, add a new op to the transaction + if comment_options or beneficiaries: + options = keep_in_dict(comment_options or {}, [ + 'max_accepted_payout', 'percent_steem_dollars', 'allow_votes', + 'allow_curation_rewards', 'extensions' + ]) + # override beneficiaries extension + if beneficiaries: + # validate schema + # or just simply vo.Schema([{'account': str, 'weight': int}]) + + for b in beneficiaries: + if 'account' not in b: + raise ValueError( + "beneficiaries need an account field!" + ) + if 'weight' not in b: + b['weight'] = 10000 + if len(b['account']) > 16: + raise ValueError( + "beneficiaries error, account name length >16!" + ) + if b['weight'] < 1 or b['weight'] > 10000: + raise ValueError( + "beneficiaries error, 1<=weight<=10000!" + ) + + options['beneficiaries'] = beneficiaries + + default_max_payout = "1000000.000 SBD" + comment_op = operations.Comment_options( + **{ + "author": + author, + "permlink": + permlink, + "max_accepted_payout": + options.get("max_accepted_payout", default_max_payout), + "percent_steem_dollars": + int(options.get("percent_steem_dollars", 10000)), + "allow_votes": + options.get("allow_votes", True), + "allow_curation_rewards": + options.get("allow_curation_rewards", True), + "extensions": + options.get("extensions", []), + "beneficiaries": + options.get("beneficiaries"), + }) + ops.append(comment_op) + + if self_vote: + vote_op = operations.Vote( + **{ + 'voter': author, + 'author': author, + 'permlink': permlink, + 'weight': 10000, + }) + ops.append(vote_op) + + return self.steem.finalizeOp(ops, account, "posting") + + def resteem(self, identifier=None, account=None): + """ Resteem a post + :param str identifier: post identifier (@<account>/<permlink>) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + if not account: + account = self.steem.configStorage.get("default_account") + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + if identifier is None: + identifier = self.identifier + author, permlink = resolve_authorperm(identifier) + json_body = [ + "reblog", { + "account": account["name"], + "author": author, + "permlink": permlink + } + ] + return self.steem.custom_json( + id="follow", json=json_body, required_posting_auths=[account["name"]]) + + def comment_options(self, options, identifier=None, account=None): + """ Set the comment options + :param str identifier: Post identifier + :param dict options: The options to define. + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + For the options, you have these defaults::: + { + "author": "", + "permlink": "", + "max_accepted_payout": "1000000.000 SBD", + "percent_steem_dollars": 10000, + "allow_votes": True, + "allow_curation_rewards": True, + } + """ + if not account: + account = self.steem.configStorage.get("default_account") + if not account: + raise ValueError("You need to provide an account") + account = Account(account, steem_instance=self.steem) + if identifier is None: + identifier = self.identifier + author, permlink = resolve_authorperm(identifier) + default_max_payout = "1000000.000 SBD" + STEEMIT_100_PERCENT = 10000 + STEEMIT_1_PERCENT = (STEEMIT_100_PERCENT / 100) + op = operations.Comment_options( + **{ + "author": + author, + "permlink": + permlink, + "max_accepted_payout": + options.get("max_accepted_payout", default_max_payout), + "percent_steem_dollars": + options.get("percent_steem_dollars", 100) * STEEMIT_1_PERCENT, + "allow_votes": + options.get("allow_votes", True), + "allow_curation_rewards": + options.get("allow_curation_rewards", True), + }) + return self.commit.finalizeOp(op, account, "posting") + class RecentReplies(list): """ Obtain a list of recent replies diff --git a/beem/discussions.py b/beem/discussions.py index 37c2788f..47b4fd9c 100644 --- a/beem/discussions.py +++ b/beem/discussions.py @@ -6,6 +6,12 @@ import logging log = logging.getLogger(__name__) +class Query(dict): + def __init__(self, limit=0, truncate_body=0): + self["limit"] = limit + self["truncate_body"] = truncate_body + + class Discussions_by_trending(list): """ get_discussions_by_trending @@ -21,3 +27,224 @@ class Discussions_by_trending(list): for x in posts ] ) + + +class Comment_discussions_by_payout(list): + """ get_comment_discussions_by_payout + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_comment_discussions_by_payout(discussion_query) + super(Comment_discussions_by_payout, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Post_discussions_by_payout(list): + """ get_post_discussions_by_payout + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_post_discussions_by_payout(discussion_query) + super(Post_discussions_by_payout, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_created(list): + """ get_discussions_by_created + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_created(discussion_query) + super(Discussions_by_created, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_active(list): + """ get_discussions_by_active + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_active(discussion_query) + super(Discussions_by_active, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_cashout(list): + """ get_discussions_by_cashout + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_cashout(discussion_query) + super(Discussions_by_cashout, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_payout(list): + """ get_discussions_by_payout + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_payout(discussion_query) + super(Discussions_by_payout, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_votes(list): + """ get_discussions_by_votes + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_votes(discussion_query) + super(Discussions_by_votes, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_children(list): + """ get_discussions_by_children + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_children(discussion_query) + super(Discussions_by_children, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_hot(list): + """ get_discussions_by_hot + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_hot(discussion_query) + super(Discussions_by_hot, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_feed(list): + """ get_discussions_by_feed + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_feed(discussion_query) + super(Discussions_by_feed, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_blog(list): + """ get_discussions_by_blog + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_blog(discussion_query) + super(Discussions_by_blog, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_comments(list): + """ get_discussions_by_comments + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_comments(discussion_query) + super(Discussions_by_comments, self).__init__( + [ + Comment(x) + for x in posts + ] + ) + + +class Discussions_by_promoted(list): + """ get_discussions_by_promoted + + :param str discussion_query + :param steem steem_instance: Steem() instance to use when accesing a RPC + """ + def __init__(self, discussion_query, steem_instance=None): + self.steem = steem_instance or shared_steem_instance() + posts = self.steem.rpc.get_discussions_by_promoted(discussion_query) + super(Discussions_by_promoted, self).__init__( + [ + Comment(x) + for x in posts + ] + ) diff --git a/beem/exceptions.py b/beem/exceptions.py index 55ee9caf..52849224 100644 --- a/beem/exceptions.py +++ b/beem/exceptions.py @@ -47,6 +47,12 @@ class InsufficientAuthorityError(Exception): pass +class VotingInvalidOnArchivedPost(Exception): + """ The transaction requires signature of a higher authority + """ + pass + + class MissingKeyError(Exception): """ A required key couldn't be found in the wallet """ diff --git a/beem/market.py b/beem/market.py index 7b2b82ce..7eb7546b 100644 --- a/beem/market.py +++ b/beem/market.py @@ -7,6 +7,7 @@ from .amount import Amount from .price import Price, Order, FilledOrder from .account import Account from beembase import operations +import random class Market(dict): @@ -43,24 +44,14 @@ class Market(dict): def __init__( self, *args, - base=None, - quote=None, steem_instance=None, **kwargs ): self.steem = steem_instance or shared_steem_instance() - if len(args) == 1 and isinstance(args[0], str): - quote_symbol, base_symbol = assets_from_string(args[0]) - quote = Asset(quote_symbol, steem_instance=self.steem) - base = Asset(base_symbol, steem_instance=self.steem) - super(Market, self).__init__({"base": base, "quote": quote}) - elif len(args) == 0 and base and quote: - super(Market, self).__init__({"base": base, "quote": quote}) - elif len(args) == 2 and not base and not quote: - super(Market, self).__init__({"base": args[1], "quote": args[0]}) - else: - raise ValueError("Unknown Market Format: %s" % str(args)) + quote = Asset("STEEM", steem_instance=self.steem) + base = Asset("SBD", steem_instance=self.steem) + super(Market, self).__init__({"base": base, "quote": quote}) def get_string(self, separator=":"): """ Return a formated string that identifies the market, e.g. ``USD:BTS`` @@ -69,23 +60,7 @@ class Market(dict): """ return "%s%s%s" % (self["quote"]["symbol"], separator, self["base"]["symbol"]) - def __eq__(self, other): - if isinstance(other, str): - quote_symbol, base_symbol = assets_from_string(other) - return ( - self["quote"]["symbol"] == quote_symbol and - self["base"]["symbol"] == base_symbol - ) or ( - self["quote"]["symbol"] == base_symbol and - self["base"]["symbol"] == quote_symbol - ) - elif isinstance(other, Market): - return ( - self["quote"]["symbol"] == other["quote"]["symbol"] and - self["base"]["symbol"] == other["base"]["symbol"] - ) - - def ticker(self): + def ticker(self, raw_data=False): """ Returns the ticker for all markets. Output Parameters: @@ -119,20 +94,13 @@ class Market(dict): """ data = {} # Core Exchange rate - cer = self["quote"]["options"]["core_exchange_rate"] - data["core_exchange_rate"] = Price( - cer, - steem_instance=self.steem - ) - if cer["base"]["asset_id"] == self["quote"]["id"]: - data["core_exchange_rate"] = data["core_exchange_rate"].invert() + self.steem.register_apis(["market_history"]) + ticker = self.steem.rpc.get_ticker(api="market_history") + if raw_data: + return ticker - ticker = self.steem.rpc.get_ticker( - self["base"]["id"], - self["quote"]["id"], - ) - data["baseVolume"] = Amount(ticker["base_volume"], self["base"], steem_instance=self.steem) - data["quoteVolume"] = Amount(ticker["quote_volume"], self["quote"], steem_instance=self.steem) + data["sbdVolume"] = Amount(ticker["sbd_volume"], steem_instance=self.steem) + data["steemVolume"] = Amount(ticker["steem_volume"], steem_instance=self.steem) data["lowestAsk"] = Price( ticker["lowest_ask"], base=self["base"], @@ -155,7 +123,7 @@ class Market(dict): return data - def volume24h(self): + def volume24h(self, raw_data=False): """ Returns the 24-hour volume for all markets, plus totals for primary currencies. Sample output: @@ -168,16 +136,53 @@ class Market(dict): } """ - volume = self.steem.rpc.get_24_volume( - self["base"]["id"], - self["quote"]["id"], - ) + self.steem.register_apis(["market_history"]) + volume = self.steem.rpc.get_volume(api="market_history") + if raw_data: + return volume return { - self["base"]["symbol"]: Amount(volume["base_volume"], self["base"], steem_instance=self.steem), - self["quote"]["symbol"]: Amount(volume["quote_volume"], self["quote"], steem_instance=self.steem) + self["base"]["symbol"]: Amount(volume["sbd_volume"], steem_instance=self.steem), + self["quote"]["symbol"]: Amount(volume["steem_volume"], steem_instance=self.steem) } - def orderbook(self, limit=25): + def orderbook(self, limit=25, raw_data=False): + """ Returns the order book for a given market. You may also + specify "all" to get the orderbooks of all markets. + :param int limit: Limit the amount of orders (default: 25) + Sample output: + .. code-block:: js + {'bids': [0.003679 USD/BTS (1.9103 USD|519.29602 BTS), + 0.003676 USD/BTS (299.9997 USD|81606.16394 BTS), + 0.003665 USD/BTS (288.4618 USD|78706.21881 BTS), + 0.003665 USD/BTS (3.5285 USD|962.74409 BTS), + 0.003665 USD/BTS (72.5474 USD|19794.41299 BTS)], + 'asks': [0.003738 USD/BTS (36.4715 USD|9756.17339 BTS), + 0.003738 USD/BTS (18.6915 USD|5000.00000 BTS), + 0.003742 USD/BTS (182.6881 USD|48820.22081 BTS), + 0.003772 USD/BTS (4.5200 USD|1198.14798 BTS), + 0.003799 USD/BTS (148.4975 USD|39086.59741 BTS)]} + .. note:: Each bid is an instance of + class:`steem.price.Order` and thus carries the keys + ``base``, ``quote`` and ``price``. From those you can + obtain the actual amounts for sale + """ + orders = self.steem.rpc.get_order_book(limit) + if raw_data: + return orders + asks = list(map(lambda x: Order( + Amount(x["order_price"]["quote"], steem_instance=self.steem), + Amount(x["order_price"]["base"], steem_instance=self.steem), + steem_instance=self.steem), orders["asks"])) + bids = list(map(lambda x: Order( + Amount(x["order_price"]["quote"], steem_instance=self.steem), + Amount(x["order_price"]["base"], steem_instance=self.steem), + steem_instance=self.steem), orders["bids"])) + asks_date = list(map(lambda x: formatTimeString(x["created"]), orders["asks"])) + bids_date = list(map(lambda x: formatTimeString(x["created"]), orders["bids"])) + data = {"asks": asks, "bids": bids, "asks_date": asks_date, "bids_date": bids_date} + return data + + def recent_trades(self, limit=25, raw_data=False): """ Returns the order book for a given market. You may also specify "all" to get the orderbooks of all markets. @@ -205,25 +210,20 @@ class Market(dict): obtain the actual amounts for sale """ - orders = self.steem.rpc.get_order_book( - self["base"]["id"], - self["quote"]["id"], - limit - ) - asks = list(map(lambda x: Order( - Amount(x["quote"], self["quote"], steem_instance=self.steem), - Amount(x["base"], self["base"], steem_instance=self.steem), - steem_instance=self.steem - ), orders["asks"])) - bids = list(map(lambda x: Order( - Amount(x["quote"], self["quote"], steem_instance=self.steem), - Amount(x["base"], self["base"], steem_instance=self.steem), - steem_instance=self.steem - ), orders["bids"])) - data = {"asks": asks, "bids": bids} - return data + self.steem.register_apis(["market_history"]) + orders = self.steem.rpc.get_recent_trades(limit, api="market_history") + if raw_data: + return orders + data_order = list(map(lambda x: Order( + Amount(x["open_pays"], steem_instance=self.steem), + Amount(x["current_pays"], steem_instance=self.steem), + steem_instance=self.steem), orders)) + + data_date = list(map(lambda x: formatTimeString(x["date"]), orders)) - def trades(self, limit=25, start=None, stop=None): + return {'date': data_date, 'order': data_order} + + def trades(self, limit=25, start=None, stop=None, raw_data=False): """ Returns your trade history for a given market. :param int limit: Limit the amount of orders (default: 25) @@ -237,68 +237,42 @@ class Market(dict): stop = datetime.now() if not start: start = stop - timedelta(hours=24) + self.steem.register_apis(["market_history"]) orders = self.steem.rpc.get_trade_history( - self["base"]["symbol"], - self["quote"]["symbol"], - formatTime(stop), - formatTime(start), - limit) - return list(map( - lambda x: FilledOrder( - x, - quote=Amount(x["amount"], self["quote"], steem_instance=self.steem), - base=Amount(float(x["amount"]) * float(x["price"]), self["base"], steem_instance=self.steem), - steem_instance=self.steem - ), orders - )) - - def accounttrades(self, account=None, limit=25): - """ Returns your trade history for a given market, specified by - the "currencyPair" parameter. You may also specify "all" to - get the orderbooks of all markets. - - :param str currencyPair: Return results for a particular market only (default: "all") - :param int limit: Limit the amount of orders (default: 25) - - Output Parameters: - - - `type`: sell or buy - - `rate`: price for `quote` denoted in `base` per `quote` - - `amount`: amount of quote - - `total`: amount of base at asked price (amount/price) - - .. note:: This call goes through the trade history and - searches for your account, if there are no orders - within ``limit`` trades, this call will return an - empty array. - - """ - if not account: - if "default_account" in self.steem.config: - account = self.steem.config["default_account"] - if not account: - raise ValueError("You need to provide an account") - account = Account(account, steem_instance=self.steem) - - filled = self.steem.rpc.get_fill_order_history( - self["base"]["id"], - self["quote"]["id"], - 2 * limit, - api="history" - ) - trades = [] - for f in filled: - if f["op"]["account_id"] == account["id"]: - trades.append( - FilledOrder( - f, - base=self["base"], - quote=self["quote"], - steem_instance=self.steem - )) - return trades - - def accountopenorders(self, account=None): + formatTimeFromNow(start), + formatTimeFromNow(stop), + limit, api="market_history") + if raw_data: + return orders + data_order = list(map(lambda x: Order( + Amount(x["open_pays"], steem_instance=self.steem), + Amount(x["current_pays"], steem_instance=self.steem), + steem_instance=self.steem), orders)) + + data_date = list(map(lambda x: formatTimeString(x["date"]), orders)) + + return {'date': data_date, 'order': data_order} + + def market_history_buckets(self): + self.steem.register_apis(["market_history"]) + return self.steem.rpc.get_market_history_buckets(api="market_history") + + def market_history(self, bucket_seconds=300, start_age=3600, end_age=0): + buckets = self.market_history_buckets() + # self.steem.register_apis(["market_history"]) + if bucket_seconds < 5 and bucket_seconds >= 0: + bucket_seconds = buckets[bucket_seconds] + else: + if bucket_seconds not in buckets: + raise ValueError("You need select the bucket_seconds from " + str(buckets)) + history = self.steem.rpc.get_market_history( + bucket_seconds, + formatTimeFromNow(-start_age - end_age), + formatTimeFromNow(-end_age), + api="market_history") + return history + + def accountopenorders(self, account=None, raw_data=False): """ Returns open Orders :param steem.account.Account account: Account name or instance of Account to show orders for in this market @@ -311,19 +285,20 @@ class Market(dict): account = Account(account, full=True, steem_instance=self.steem) r = [] - orders = account["limit_orders"] + # orders = account["limit_orders"] + orders = self.steem.rpc.get_open_orders(account["name"]) + if raw_data: + return orders for o in orders: - if (( - o["sell_price"]["base"]["asset_id"] == self["base"]["id"] and - o["sell_price"]["quote"]["asset_id"] == self["quote"]["id"] - ) or ( - o["sell_price"]["base"]["asset_id"] == self["quote"]["id"] and - o["sell_price"]["quote"]["asset_id"] == self["base"]["id"] - )): - r.append(Order( - o, - steem_instance=self.steem - )) + order = {} + order["order"] = Order( + Amount(o["sell_price"]["base"]), + Amount(o["sell_price"]["quote"]), + steem_instance=self.steem + ) + order["orderid"] = o["orderid"] + order["created"] = formatTimeString(o["created"]) + r.append(order) return r def buy( @@ -333,6 +308,7 @@ class Market(dict): expiration=None, killfill=False, account=None, + orderid=None, returnOrderId=False ): """ Places a buy order in a given market @@ -386,20 +362,22 @@ class Market(dict): assert(amount["asset"]["symbol"] == self["quote"]["symbol"]), \ "Price: {} does not match amount: {}".format( str(price), str(amount)) + elif isinstance(amount, str): + amount = Amount(amount, steem_instance=self.steem) else: amount = Amount(amount, self["quote"]["symbol"], steem_instance=self.steem) order = operations.Limit_order_create(**{ - "fee": {"amount": 0, "asset_id": "1.3.0"}, - "seller": account["id"], - "amount_to_sell": { - "amount": int(float(amount) * float(price) * 10 ** self["base"]["precision"]), - "asset_id": self["base"]["id"] - }, - "min_to_receive": { - "amount": int(float(amount) * 10 ** self["quote"]["precision"]), - "asset_id": self["quote"]["id"] - }, + "owner": account["name"], + "orderid": orderid or random.getrandbits(32), + "amount_to_sell": Amount( + float(amount) * float(price), + self["base"]["symbol"] + ), + "min_to_receive": Amount( + float(amount), + self["quote"]["symbol"] + ), "expiration": formatTimeFromNow(expiration), "fill_or_kill": killfill, }) @@ -424,6 +402,7 @@ class Market(dict): expiration=None, killfill=False, account=None, + orderid=None, returnOrderId=False ): """ Places a sell order in a given market @@ -464,20 +443,22 @@ class Market(dict): assert(amount["asset"]["symbol"] == self["quote"]["symbol"]), \ "Price: {} does not match amount: {}".format( str(price), str(amount)) + elif isinstance(amount, str): + amount = Amount(amount, steem_instance=self.steem) else: amount = Amount(amount, self["quote"]["symbol"], steem_instance=self.steem) order = operations.Limit_order_create(**{ - "fee": {"amount": 0, "asset_id": "1.3.0"}, - "seller": account["id"], - "amount_to_sell": { - "amount": int(float(amount) * 10 ** self["quote"]["precision"]), - "asset_id": self["quote"]["id"] - }, - "min_to_receive": { - "amount": int(float(amount) * float(price) * 10 ** self["base"]["precision"]), - "asset_id": self["base"]["id"] - }, + "owner": account["name"], + "orderid": orderid or random.getrandbits(32), + "amount_to_sell": Amount( + float(amount), + self["quote"]["symbol"] + ), + "min_to_receive": Amount( + float(amount) * float(price), + self["base"]["symbol"] + ), "expiration": formatTimeFromNow(expiration), "fill_or_kill": killfill, }) @@ -494,41 +475,26 @@ class Market(dict): return tx - def cancel(self, orderNumber, account=None): + def cancel(self, orderNumbers, account=None, **kwargs): """ Cancels an order you have placed in a given market. Requires - only the "orderNumber". An order number takes the form + only the "orderNumbers". An order number takes the form ``1.7.xxx``. - - :param str orderNumber: The Order Object ide of the form ``1.7.xxxx`` - """ - return self.steem.cancel(orderNumber, account=account) - - def core_quote_market(self): - """ This returns an instance of the market that has the core market of the quote asset. - It means that quote needs to be a market pegged asset and returns a - market to it's collateral asset. + :param str orderNumbers: The Order Object ide of the form ``1.7.xxxx`` """ - if not self["quote"].is_bitasset: - raise ValueError("Quote (%s) is not a bitasset!" % self["quote"]["symbol"]) - self["quote"].full = True - self["quote"].refresh() - collateral = Asset( - self["quote"]["bitasset_data"]["options"]["short_backing_asset"], - steem_instance=self.steem - ) - return Market(quote=self["quote"], base=collateral) - - def core_base_market(self): - """ This returns an instance of the market that has the core market of the base asset. - It means that base needs to be a market pegged asset and returns a - market to it's collateral asset. - """ - if not self["base"].is_bitasset: - raise ValueError("base (%s) is not a bitasset!" % self["base"]["symbol"]) - self["base"].full = True - self["base"].refresh() - collateral = Asset( - self["base"]["bitasset_data"]["options"]["short_backing_asset"], - steem_instance=self.steem - ) - return Market(quote=self["base"], base=collateral) + if not account: + if "default_account" in self.steem.config: + account = self.steem.config["default_account"] + if not account: + raise ValueError("You need to provide an account") + account = Account(account, full=False, steem_instance=self.steem) + + if not isinstance(orderNumbers, (list, set, tuple)): + orderNumbers = {orderNumbers} + + op = [] + for order in orderNumbers: + op.append( + operations.Limit_order_cancel(**{ + "owner": account["name"], + "orderid": order})) + return self.steem.finalizeOp(op, account["name"], "active", **kwargs) diff --git a/beem/steem.py b/beem/steem.py index c5c6ea03..b84e8a40 100644 --- a/beem/steem.py +++ b/beem/steem.py @@ -17,7 +17,7 @@ from .exceptions import ( ) from .wallet import Wallet from .transactionbuilder import TransactionBuilder -from .utils import formatTime +from .utils import formatTime, resolve_authorperm log = logging.getLogger(__name__) @@ -108,6 +108,7 @@ class Steem(object): rpcuser="", rpcpassword="", debug=False, + data_refresh_time_seconds=900, **kwargs): # More specific set of APIs to register to @@ -115,9 +116,9 @@ class Steem(object): kwargs["apis"] = [ "database", "network_broadcast", - # "market_history", - # "follow", - # "account_by_key", + "market_history", + "follow", + "account_by_key", # "tag", # "raw_block" ] @@ -142,13 +143,17 @@ class Steem(object): **kwargs) # Try Optional APIs - try: - self.rpc.register_apis(["account_by_key", "follow"]) - except NoAccessApi as e: - log.info(str(e)) + self.register_apis() self.wallet = Wallet(self.rpc, **kwargs) + self.data = {'last_refresh': None, 'dynamic_global_properties': None, 'feed_history': None, + 'current_median_history_price': None, 'next_scheduled_hardfork': None, + 'hardfork_version': None, 'network': None, 'chain_properties': None, + 'config': None, 'reward_fund': None} + self.data_refresh_time_seconds = data_refresh_time_seconds + # self.refresh_data() + # txbuffers/propbuffer are initialized and cleared self.clear() @@ -176,6 +181,205 @@ class Steem(object): self.rpc = SteemNodeRPC(node, rpcuser, rpcpassword, **kwargs) + def register_apis(self, apis=["network_broadcast", "account_by_key", "follow", "market_history"]): + + # Try Optional APIs + try: + self.rpc.register_apis(apis) + except NoAccessApi as e: + log.info(str(e)) + + def refresh_data(self, force_refresh=False): + if self.offline: + return + if self.data['last_refresh'] is not None and not force_refresh: + if (datetime.now() - self.data['last_refresh']).total_seconds() < self.data_refresh_time_seconds: + return + self.data['last_refresh'] = datetime.now() + self.data["dynamic_global_properties"] = self.get_dynamic_global_properties(False) + self.data['feed_history'] = self.get_feed_history(False) + self.data['current_median_history_price'] = self.get_current_median_history_price(False) + self.data['next_scheduled_hardfork'] = self.get_next_scheduled_hardfork(False) + self.data['hardfork_version'] = self.get_hardfork_version(False) + self.data['network'] = self.get_network(False) + self.data['chain_properties'] = self.get_chain_properties(False) + self.data['config'] = self.get_config(False) + self.data['reward_fund'] = {"post": self.get_reward_fund("post", False)} + + def get_dynamic_global_properties(self, use_stored_data=True): + """ This call returns the *dynamic global properties* + """ + if use_stored_data: + self.refresh_data() + return self.data['dynamic_global_properties'] + else: + return self.rpc.get_dynamic_global_properties() + + def get_feed_history(self, use_stored_data=True): + """ Returns the feed_history + """ + if use_stored_data: + self.refresh_data() + return self.data['feed_history'] + else: + return self.rpc.get_feed_history() + + def get_reward_fund(self, fund_name="post", use_stored_data=True): + """ Get details for a reward fund. + """ + if use_stored_data: + self.refresh_data() + return self.data['reward_fund'][fund_name] + else: + return self.rpc.get_reward_fund(fund_name) + + def get_current_median_history_price(self, use_stored_data=True): + """ Returns the current median price + """ + if use_stored_data: + self.refresh_data() + return self.data['current_median_history_price'] + else: + return self.rpc.get_current_median_history_price() + + def get_next_scheduled_hardfork(self, use_stored_data=True): + """ Returns Hardfork and live_time of the hardfork + """ + if use_stored_data: + self.refresh_data() + return self.data['next_scheduled_hardfork'] + else: + return self.rpc.get_next_scheduled_hardfork() + + def get_hardfork_version(self, use_stored_data=True): + """ Current Hardfork Version as String + """ + if use_stored_data: + self.refresh_data() + return self.data['hardfork_version'] + else: + return self.rpc.get_hardfork_version() + + def get_network(self, use_stored_data=True): + """ Identify the network + + :returns: Network parameters + :rtype: dict + """ + if use_stored_data: + self.refresh_data() + return self.data['network'] + else: + return self.rpc.get_network() + + def get_median_price(self): + median_price = self.get_current_median_history_price() + a = ( + Amount(median_price['base']) / + Amount(median_price['quote']) + ) + return a.as_base("SBD") + + def get_payout_from_rshares(self, rshares): + reward_fund = self.get_reward_fund() + reward_balance = Amount(reward_fund["reward_balance"]).amount + recent_claims = float(reward_fund["recent_claims"]) + + fund_per_share = reward_balance / (recent_claims) + SBD_price = (self.get_median_price() * Amount("1 STEEM")).amount + payout = float(rshares) * fund_per_share * SBD_price + return payout + + def get_steem_per_mvest(self, time_stamp=None): + + if time_stamp is not None: + a = 2.1325476281078992e-05 + b = -31099.685481490847 + a2 = 2.9019227739473682e-07 + b2 = 48.41432402074669 + + if (time_stamp < (b2 - b) / (a - a2)): + return a * time_stamp + b + else: + return a2 * time_stamp + b2 + global_properties = self.get_dynamic_global_properties() + + return ( + Amount(global_properties['total_vesting_fund_steem']).amount / + (Amount(global_properties['total_vesting_shares']).amount / 1e6) + ) + + def vests_to_sp(self, vests, timestamp=None): + if isinstance(vests, Amount): + vests = vests.amount + return vests / 1e6 * self.get_steem_per_mvest(timestamp) + + def sp_to_vests(self, sp, timestamp=None): + return sp * 1e6 / self.get_steem_per_mvest(timestamp) + + def sp_to_sbd(self, sp, voting_power=10000, vote_pct=10000): + reward_fund = self.get_reward_fund() + reward_balance = Amount(reward_fund["reward_balance"]).amount + recent_claims = float(reward_fund["recent_claims"]) + reward_share = reward_balance / recent_claims + + resulting_vote = ((voting_power * (vote_pct) / 10000) + 49) / 50 + vesting_shares = int(self.sp_to_vests(sp)) + SBD_price = (self.get_median_price() * Amount("1 STEEM")).amount + VoteValue = (vesting_shares * resulting_vote * 100) * reward_share * SBD_price + return VoteValue + + def sp_to_rshares(self, sp, voting_power=10000, vote_pct=10000): + """ Obtain the r-shares + :param number sp: Steem Power + :param int voting_power: voting power (100% = 10000) + :param int vote_pct: voting participation (100% = 10000) + """ + # calculate our account voting shares (from vests), mine is 6.08b + vesting_shares = int(self.sp_to_vests(sp) * 1e6) + + # get props + global_properties = self.get_dynamic_global_properties() + vote_power_reserve_rate = global_properties['vote_power_reserve_rate'] + + # determine voting power used + used_power = int((voting_power * vote_pct) / 10000) + max_vote_denom = vote_power_reserve_rate * (5 * 60 * 60 * 24) / (60 * 60 * 24) + used_power = int((used_power + max_vote_denom - 1) / max_vote_denom) + # calculate vote rshares + rshares = ((vesting_shares * used_power) / 10000) + + return rshares + + def get_chain_properties(self, use_stored_data=True): + """ Return witness elected chain properties + + :: + {'account_creation_fee': '30.000 STEEM', + 'maximum_block_size': 65536, + 'sbd_interest_rate': 250} + + """ + if use_stored_data: + self.refresh_data() + return self.data['chain_properties'] + else: + return self.rpc.get_chain_properties() + + def get_state(self, path="value"): + """ get_state + """ + return self.rpc.get_state(path) + + def get_config(self, use_stored_data=True): + """ Returns internal chain configuration. + """ + if use_stored_data: + self.refresh_data() + return self.data['config'] + else: + return self.rpc.get_config() + @property def chain_params(self): if self.offline: @@ -341,44 +545,6 @@ class Steem(object): self.new_tx() # self.new_proposal() - # ------------------------------------------------------------------------- - # Simple Transfer - # ------------------------------------------------------------------------- - def transfer(self, to, amount, asset, memo="", account=None, **kwargs): - """ Transfer an asset to another account. - - :param str to: Recipient - :param float amount: Amount to transfer - :param str asset: Asset to transfer - :param str memo: (optional) Memo, may begin with `#` for encrypted - messaging - :param str account: (optional) the source account for the transfer - if not ``default_account`` - """ - from .memo import Memo - if not account: - if "default_account" in config: - account = config["default_account"] - if not account: - raise ValueError("You need to provide an account") - - account = Account(account, steem_instance=self) - amount = Amount(amount, asset, steem_instance=self) - to = Account(to, steem_instance=self) - - memoObj = Memo( - from_account=account, - to_account=to, - steem_instance=self - ) - op = operations.Transfer(**{ - "amount": amount, - "to": to["name"], - "memo": memoObj.encrypt(memo), - "from": account["name"], - }) - return self.finalizeOp(op, account, "active", **kwargs) - # ------------------------------------------------------------------------- # Account related calls # ------------------------------------------------------------------------- @@ -640,9 +806,9 @@ class Steem(object): "json_metadata": account["json_metadata"], }) if permission == "owner": - return self.finalizeOp(op, account["name"], "owner", **kwargs) + return self.finalizeOp(op, account, "owner", **kwargs) else: - return self.finalizeOp(op, account["name"], "active", **kwargs) + return self.finalizeOp(op, account, "active", **kwargs) def disallow( self, foreign, permission="posting", @@ -723,73 +889,35 @@ class Steem(object): "json_metadata": account["json_metadata"] }) if permission == "owner": - return self.finalizeOp(op, account["name"], "owner", **kwargs) + return self.finalizeOp(op, account, "owner", **kwargs) else: - return self.finalizeOp(op, account["name"], "active", **kwargs) - - def update_memo_key(self, key, account=None, **kwargs): - """ Update an account's memo public key - - This method does **not** add any private keys to your - wallet but merely changes the memo public key. - - :param str key: New memo public key - :param str account: (optional) the account to allow access - to (defaults to ``default_account``) - """ - if not account: - if "default_account" in config: - account = config["default_account"] - if not account: - raise ValueError("You need to provide an account") - - PublicKey(key, prefix=self.prefix) - - account = Account(account, steem_instance=self) - account["memo_key"] = key - op = operations.Account_update(**{ - "account": account["name"], - "memo_key": account["memo_key"], - "json_metadata": account["json_metadata"] - }) - return self.finalizeOp(op, account["name"], "active", **kwargs) - - # ------------------------------------------------------------------------- - # Approval and Disapproval of witnesses - # ------------------------------------------------------------------------- - def approvewitness(self, witness, account=None, approve=True, **kwargs): - """ Approve a witness - - :param list witnesses: list of Witness name or id - :param str account: (optional) the account to allow access - to (defaults to ``default_account``) - """ - if not account: - if "default_account" in config: - account = config["default_account"] - if not account: - raise ValueError("You need to provide an account") - account = Account(account, steem_instance=self) - - # if not isinstance(witnesses, (list, set, tuple)): - # witnesses = {witnesses} - - # for witness in witnesses: - # witness = Witness(witness, steem_instance=self) - - op = operations.Account_witness_vote(**{ - "account": account["name"], - "witness": witness, - "approve": approve - }) - return self.finalizeOp(op, account["name"], "active", **kwargs) - - def disapprovewitness(self, witness, account=None, **kwargs): - """ Disapprove a witness - - :param list witnesses: list of Witness name or id - :param str account: (optional) the account to allow access - to (defaults to ``default_account``) + return self.finalizeOp(op, account, "active", **kwargs) + + def custom_json(self, + id, + json_data, + required_auths=[], + required_posting_auths=[]): + """ Create a custom json operation + :param str id: identifier for the custom json (max length 32 bytes) + :param json json_data: the json data to put into the custom_json + operation + :param list required_auths: (optional) required auths + :param list required_posting_auths: (optional) posting auths """ - return self.approvewitness( - witness=witness, account=account, approve=False) + account = None + if len(required_auths): + account = required_auths[0] + elif len(required_posting_auths): + account = required_posting_auths[0] + else: + raise Exception("At least one account needs to be specified") + account = Account(account, full=False, steem_instance=self) + op = operations.Custom_json( + **{ + "json": json_data, + "required_auths": required_auths, + "required_posting_auths": required_posting_auths, + "id": id + }) + return self.finalizeOp(op, account, "posting") diff --git a/beem/transactionbuilder.py b/beem/transactionbuilder.py index 07a4577c..c4c89e8f 100644 --- a/beem/transactionbuilder.py +++ b/beem/transactionbuilder.py @@ -269,7 +269,7 @@ class TransactionBuilder(dict): log.warning("Not broadcasting anything!") self.clear() return ret - + self.steem.register_apis(["network_broadcast"]) # Broadcast try: if self.steem.blocking: diff --git a/beem/utils.py b/beem/utils.py index 5fc5330d..53c4e955 100644 --- a/beem/utils.py +++ b/beem/utils.py @@ -1,6 +1,7 @@ import re import time from datetime import datetime +import difflib from .exceptions import ObjectNotInProposalBuffer timeFormat = '%Y-%m-%dT%H:%M:%S' @@ -111,6 +112,17 @@ def construct_authorperm(*args, username_prefix='@'): return "{prefix}{author}/{permlink}".format(**fields) +def resolve_root_identifier(url): + m = re.match("/([^/]*)/@([^/]*)/([^#]*).*", url) + if not m: + return "", "" + else: + category = m.group(1) + author = m.group(2) + permlink = m.group(3) + return construct_authorperm(author, permlink), category + + def resolve_authorpermvoter(identifier): """Correctly split a string containing an authorpermvoter. @@ -176,3 +188,37 @@ def test_proposal_in_buffer(buf, operation_name, id): id ) ) + + +def keep_in_dict(obj, allowed_keys=list()): + """ Prune a class or dictionary of all but allowed keys. + """ + if type(obj) == dict: + items = obj.items() + else: + items = obj.__dict__.items() + + return {k: v for k, v in items if k in allowed_keys} + + +def remove_from_dict(obj, remove_keys=list()): + """ Prune a class or dictionary of specified keys. + """ + if type(obj) == dict: + items = obj.items() + else: + items = obj.__dict__.items() + + return {k: v for k, v in items if k not in remove_keys} + + +def make_patch(a, b, n=3): + # _no_eol = '\n' + "\ No newline at end of file" + '\n' + _no_eol = '\n' + diffs = difflib.unified_diff(a.splitlines(True), b.splitlines(True), n=n) + try: + _, _ = next(diffs), next(diffs) + del _ + except StopIteration: + pass + return ''.join([d if d[-1] == '\n' else d + _no_eol for d in diffs]) diff --git a/beem/wallet.py b/beem/wallet.py index 1d957a0e..59e0ce4f 100644 --- a/beem/wallet.py +++ b/beem/wallet.py @@ -10,8 +10,9 @@ from .exceptions import ( WalletLocked, WrongMasterPasswordException, NoWalletException, - RPCConnectionRequired + RPCConnectionRequired, ) +from beemapi.exceptions import NoAccessApi log = logging.getLogger(__name__) @@ -51,6 +52,7 @@ class Wallet(): from beem import Steem steem = Steem() + steem.wallet.purgeWallet() steem.wallet.create("supersecret-passphrase") This will raise an exception if you already have a wallet installed. @@ -211,6 +213,11 @@ class Wallet(): """ self.newWallet(pwd) + def purge(self): + """ Alias for purgeWallet() + """ + self.purgeWallet() + def newWallet(self, pwd): """ Create a new wallet database """ @@ -364,7 +371,11 @@ class Wallet(): def getAccountsFromPublicKey(self, pub): """ Obtain all accounts associated with a public key """ - names = self.rpc.get_key_references([pub]) + try: + self.rpc.register_apis(["account_by_key"]) + except NoAccessApi as e: + print(str(e)) + names = self.rpc.get_key_references([pub], api="account_by_key") for name in names: for i in name: yield i @@ -375,7 +386,11 @@ class Wallet(): # FIXME, this only returns the first associated key. # If the key is used by multiple accounts, this # will surely lead to undesired behavior - names = self.rpc.get_key_references([pub])[0] + try: + self.rpc.register_apis(["account_by_key"]) + except NoAccessApi as e: + print(str(e)) + names = self.rpc.get_key_references([pub], api="account_by_key")[0] if not names: return None else: @@ -415,7 +430,7 @@ class Wallet(): def getKeyType(self, account, pub): """ Get key type """ - for authority in ["owner", "active"]: + for authority in ["owner", "active", "posting"]: for key in account[authority]["key_auths"]: if pub == key[0]: return authority diff --git a/beem/witness.py b/beem/witness.py index 69e3d93c..00860d53 100644 --- a/beem/witness.py +++ b/beem/witness.py @@ -1,9 +1,12 @@ from beem.instance import shared_steem_instance from .account import Account +from .amount import Amount from .exceptions import WitnessDoesNotExistsException from .blockchainobject import BlockchainObject from .utils import formatTimeString, parse_time from datetime import datetime, timedelta +from beembase import transactions, operations +from beembase.account import PrivateKey, PublicKey class Witness(BlockchainObject): @@ -39,6 +42,8 @@ class Witness(BlockchainObject): ) def refresh(self): + if not self.identifier: + return witness = self.steem.rpc.get_witness_by_account(self.identifier) if not witness: raise WitnessDoesNotExistsException @@ -49,6 +54,86 @@ class Witness(BlockchainObject): def account(self): return Account(self["owner"], steem_instance=self.steem) + def feed_publish(self, + base, + quote="1.000 STEEM", + account=None): + """ Publish a feed price as a witness. + :param float base: USD Price of STEEM in SBD (implied price) + :param float quote: (optional) Quote Price. Should be 1.000, unless + we are adjusting the feed to support the peg. + :param str account: (optional) the source account for the transfer + if not self["owner"] + """ + if not account: + account = self["owner"] + if not account: + raise ValueError("You need to provide an account") + + account = Account(account, steem_instance=self) + if isinstance(base, Amount): + base = Amount(base, steem_instance=self.steem) + elif isinstance(base, str): + base = Amount(base, steem_instance=self.steem) + else: + base = Amount(base, "SBD", steem_instance=self.steem) + + if isinstance(quote, Amount): + quote = Amount(quote, steem_instance=self.steem) + elif isinstance(quote, str): + quote = Amount(quote, steem_instance=self.steem) + else: + quote = Amount(quote, "STEEM", steem_instance=self.steem) + + assert base.symbol == "SBD" + assert quote.symbol == "STEEM" + + op = operations.Feed_publish( + **{ + "publisher": account["name"], + "exchange_rate": { + "base": base, + "quote": quote, + } + }) + return self.steem.finalizeOp(op, account, "active") + + def update(self, signing_key, url, props, account=None): + """ Update witness + :param pubkey signing_key: Signing key + :param str url: URL + :param dict props: Properties + :param str account: (optional) witness account name + Properties::: + { + "account_creation_fee": x, + "maximum_block_size": x, + "sbd_interest_rate": x, + } + """ + if not account: + account = self["owner"] + if not account: + raise ValueError("You need to provide an account") + + account = Account(account, steem_instance=self) + + try: + PublicKey(signing_key) + except Exception as e: + raise e + + op = operations.Witness_update( + **{ + "owner": account["name"], + "url": url, + "block_signing_key": signing_key, + "props": props, + "fee": "0.000 STEEM", + "prefix": self.steem.chain_params["prefix"] + }) + return self.steem.finalizeOp(op, account, "active") + class WitnessesObject(list): def printAsTable(self, sort_key="votes", reverse=True): diff --git a/beembase/operations.py b/beembase/operations.py index 6512ca92..71dbf125 100644 --- a/beembase/operations.py +++ b/beembase/operations.py @@ -73,7 +73,7 @@ class Vote(GrapheneObject): if len(args) == 1 and len(kwargs) == 0: kwargs = args[0] super().__init__(OrderedDict([ - ('Voter', String(kwargs["voter"])), + ('voter', String(kwargs["voter"])), ('author', String(kwargs["author"])), ('permlink', String(kwargs["permlink"])), ('weight', Int16(kwargs["weight"])), diff --git a/docs/index.rst b/docs/index.rst index c8e3c2a5..70c32f00 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -86,19 +86,11 @@ Quickstart .. code-block:: python from beem.market import Market - # Not working at the moment - # market = Market("STEEM:SBD") - # print(market.ticker()) - # market.steem.wallet.unlock("wallet-passphrase") - # print(market.sell(300, 100) # sell 100 STEEM for 300 STEEM/SBD + market = Market() + print(market.ticker()) + market.steem.wallet.unlock("wallet-passphrase") + print(market.sell(300, 100) # sell 100 STEEM for 300 STEEM/SBD -.. code-block:: python - - from beem.dex import Dex - # not working at the moment - # dex = Dex() - # dex.steem.wallet.unlock("wallet-passphrase") - General ------- diff --git a/docs/tutorials.rst b/docs/tutorials.rst index db41326f..6612a9e1 100644 --- a/docs/tutorials.rst +++ b/docs/tutorials.rst @@ -17,7 +17,6 @@ executed in the same order as they are added to the transaction. from beem import Steem testnet = Steem( - "wss://testnet.steem.vc", nobroadcast=True, bundle=True, ) @@ -46,7 +45,6 @@ attribute: from beem import Steem testnet = Steem( - "wss://testnet.steem.vc", proposer="test" ) testnet.wallet.unlock("supersecret") @@ -66,7 +64,6 @@ Simple Sell Script # Instanciate Steem (pick network via API node) # steem = Steem( - "wss://node.testnet.steem.eu", nobroadcast=True # <<--- set this to False when you want to fire! ) @@ -81,7 +78,6 @@ Simple Sell Script # Sell and buy calls always refer to the *quote* # market = Market( - "GOLD:USD", steem_instance=steem ) @@ -89,8 +85,8 @@ Simple Sell Script # Sell an asset for a price with amount (quote) # print(market.sell( - Price(100.0, "USD/GOLD"), - Amount("0.01 GOLD") + Price(100.0, "STEEM/SBD"), + Amount("0.01 STEEM") )) @@ -122,7 +118,6 @@ Sell at a timely rate # Instanciate Steem (pick network via API node) # steem = Steem( - "wss://node.testnet.steem.eu", nobroadcast=True # <<--- set this to False when you want to fire! ) @@ -137,7 +132,6 @@ Sell at a timely rate # Sell and buy calls always refer to the *quote* # market = Market( - "GOLD:USD", steem_instance=steem ) diff --git a/setup.py b/setup.py index 9f875fde..3624536b 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ except LookupError: ascii = codecs.lookup('ascii') codecs.register(lambda name, enc=ascii: {True: enc}.get(name == 'mbcs')) -VERSION = '0.19.4' +VERSION = '0.19.5' def write_version_py(filename): diff --git a/tests/test_steem.py b/tests/test_steem.py index a7d5f28f..4e3e176b 100644 --- a/tests/test_steem.py +++ b/tests/test_steem.py @@ -6,6 +6,8 @@ from pprint import pprint from beem import Steem from beembase.operationids import getOperationNameForId from beem.amount import Amount +from beem.witness import Witness +from beem.account import Account from beembase.account import PrivateKey from beem.instance import set_shared_steem_instance @@ -34,7 +36,8 @@ class Testcases(unittest.TestCase): def test_transfer(self): bts = self.bts # bts.prefix ="STX" - tx = bts.transfer( + acc = Account("test", steem_instance=bts) + tx = acc.transfer( "test", 1.33, "SBD", memo="Foobar", account="test1") self.assertEqual( tx["operations"][0][0], @@ -119,9 +122,11 @@ class Testcases(unittest.TestCase): bts = self.bts tx1 = bts.new_tx() tx2 = bts.new_tx() - self.bts.transfer("test1", 1, "STEEM", append_to=tx1) - self.bts.transfer("test1", 2, "STEEM", append_to=tx2) - self.bts.transfer("test1", 3, "STEEM", append_to=tx1) + + acc = Account("test1", steem_instance=bts) + acc.transfer("test1", 1, "STEEM", append_to=tx1) + acc.transfer("test1", 2, "STEEM", append_to=tx2) + acc.transfer("test1", 3, "STEEM", append_to=tx1) tx1 = tx1.json() tx2 = tx2.json() ops1 = tx1["operations"] @@ -189,7 +194,8 @@ class Testcases(unittest.TestCase): def test_update_memo_key(self): bts = self.bts - tx = bts.update_memo_key("STM55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n") + acc = Account("test1", steem_instance=bts) + tx = acc.update_memo_key("STM55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n") self.assertEqual( (tx["operations"][0][0]), "account_update" @@ -201,7 +207,8 @@ class Testcases(unittest.TestCase): def test_approvewitness(self): bts = self.bts - tx = bts.approvewitness("test1") + w = Account("test", steem_instance=bts) + tx = w.approvewitness("test1") self.assertEqual( (tx["operations"][0][0]), "account_witness_vote" diff --git a/tests/test_utils.py b/tests/test_utils.py index 03940750..34360a7a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,7 +6,9 @@ from beem.utils import ( construct_authorperm, construct_authorpermvoter, sanitize_permlink, - derive_permlink + derive_permlink, + resolve_root_identifier, + make_patch ) @@ -15,6 +17,9 @@ class Testcases(unittest.TestCase): self.assertEqual(construct_authorperm("A", "B"), "@A/B") self.assertEqual(construct_authorperm({'author': "A", 'permlink': "B"}), "@A/B") + def test_resolve_root_identifier(self): + self.assertEqual(resolve_root_identifier("/a/@b/c"), ("@b/c", "a")) + def test_constructAuthorpermvoter(self): self.assertEqual(construct_authorpermvoter("A", "B", "C"), "@A/B|C") self.assertEqual(construct_authorpermvoter({'author': "A", 'permlink': "B", 'voter': 'C'}), "@A/B|C") @@ -44,3 +49,8 @@ class Testcases(unittest.TestCase): self.assertEqual(derive_permlink("Hello World"), "hello-world") self.assertEqual(derive_permlink("aAf_0.12"), "aaf-0-12") self.assertEqual(derive_permlink("[](){}"), "") + + def test_patch(self): + self.assertEqual(make_patch("aa", "ab"), '@@ -1 +1 @@\n-aa\n+ab\n') + self.assertEqual(make_patch("Hello!\n Das ist ein Test!\nEnd.\n", "Hello!\n This is a Test\nEnd.\n"), + '@@ -1,3 +1,3 @@\n Hello!\n- Das ist ein Test!\n+ This is a Test\n End.\n') diff --git a/tox.ini b/tox.ini index f9198763..9691a382 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py35,py36,py37,lint,docs +envlist = py{34,35,36,37},lint,docs skip_missing_interpreters = true [testenv] @@ -24,3 +24,4 @@ deps=-rdocs/requirements.txt sphinx commands= sphinx-build -b html ./ ./html + -- GitLab