diff --git a/.gitignore b/.gitignore index bd2eb18e..c07bf5af 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ target/ # Vim temp files *.swp +.ropeproject/ +*/.ropeproject/ diff --git a/.travis.yml b/.travis.yml index a0164f06..2d9438ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,9 @@ language: python -python: "3.5" +python: + - 3.5 install: - pip install tox codecov script: - tox -env: - - TOXENV=py35 after_success: - codecov diff --git a/Makefile b/Makefile index 1d5d0ccd..e8e49e20 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,5 @@ .PHONY: clean-pyc clean-build docs -TAG := $(shell git describe master --abbrev=0) -TAGSTEEM := $(shell git describe master --abbrev=0 | tr "." "-") - -# clean: clean-build clean-pyc clean-build: @@ -43,7 +39,4 @@ dist: python3 setup.py sdist upload -r pypi python3 setup.py bdist_wheel upload -release: clean check dist bitshares-changelog git - -bitshares-changelog: - git show -s --pretty=format: $(TAG) | tail -n +4 | piston post --file "-" --author chainsquad --permlink "python-bitshares-changelog-$(TAGSTEEM)" --category bitshares --title "[Changelog] python-bitshares $(TAG)" --tags python-bitshares changelog +release: clean check dist git diff --git a/README.md b/README.md index 1584d555..5b62257a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,27 @@ -Python Library for BitShares -======================== +# Python Library for BitShares -*placeholder* +--- + +## Documentation + +Visit the [pybitshares website](http://docs.pybitshares.com/en/latest/) for in depth documentation on this Python library. + +## Installation + +### Install with pip: +``` +$ sudo apt-get install libffi-dev libssl-dev python-dev python3-dev python3-pip +$ pip3 install bitshares +``` + +### Manual installation: +``` +$ git clone https://github.com/xeroc/python-bitshares/ +$ cd python-bitshares +$ python3 setup.py install --user +``` + +### Upgrade +``` +$ pip3 install --user --upgrade +``` diff --git a/bitshares/__init__.py b/bitshares/__init__.py index 5fa70f71..915ef88d 100644 --- a/bitshares/__init__.py +++ b/bitshares/__init__.py @@ -16,5 +16,6 @@ "wallet", "committee", "vesting", - "proposal" + "proposal", + "message" ] diff --git a/bitshares/account.py b/bitshares/account.py index 3871f1b0..a5b85d3b 100644 --- a/bitshares/account.py +++ b/bitshares/account.py @@ -38,16 +38,16 @@ class Account(BlockchainObject): def __init__( self, account, - lazy=False, full=False, + lazy=False, bitshares_instance=None ): self.full = full super().__init__( account, - lazy=False, - full=False, - bitshares_instance=None + lazy=lazy, + full=full, + bitshares_instance=bitshares_instance ) def refresh(self): @@ -59,9 +59,9 @@ def refresh(self): else: account = self.bitshares.rpc.lookup_account_names( [self.identifier])[0] - self.identifier = account["id"] if not account: raise AccountDoesNotExistsException(self.identifier) + self.identifier = account["id"] if self.full: account = self.bitshares.rpc.get_full_accounts( @@ -118,9 +118,7 @@ def call_positions(self): def callpositions(self): """ List call positions (collateralized positions :doc:`mpa`) """ - if not self.full: - self.full = True - self.refresh() + self.ensure_full() from .dex import Dex dex = Dex(bitshares_instance=self.bitshares) return dex.list_debt_positions(self) @@ -130,10 +128,22 @@ def openorders(self): """ Returns open Orders """ from .price import Order - if not self.full: + self.ensure_full() + return [ + Order(o, bitshares_instance=self.bitshares) + for o in self["limit_orders"] + ] + + @property + def is_fully_loaded(self): + """ Is this instance fully loaded / e.g. all data available? + """ + return (self.full and "votes" in self) + + def ensure_full(self): + if not self.is_fully_loaded: self.full = True self.refresh() - return [Order(o) for o in self["limit_orders"]] def history( self, first=None, @@ -165,7 +175,7 @@ def history( api="history" ) if not mostrecent: - raise StopIteration + return if not first: # first = int(mostrecent[0].get("id").split(".")[2]) + 1 @@ -191,7 +201,7 @@ def history( cnt += 1 yield i if limit >= 0 and cnt >= limit: - raise StopIteration + return if not txs: break diff --git a/bitshares/amount.py b/bitshares/amount.py index 8ce15ee2..967d25c1 100644 --- a/bitshares/amount.py +++ b/bitshares/amount.py @@ -91,7 +91,7 @@ def __init__(self, *args, amount=None, asset=None, bitshares_instance=None): elif isinstance(amount, (int, float)) and asset and isinstance(asset, str): self["amount"] = amount - self["asset"] = Asset(asset) + self["asset"] = Asset(asset, bitshares_instance=self.bitshares) self["symbol"] = asset else: @@ -145,7 +145,7 @@ def __str__(self): ) def __float__(self): - return self["amount"] + return float(self["amount"]) def __int__(self): return int(self["amount"] * 10 ** self["asset"]["precision"]) @@ -171,6 +171,7 @@ def __sub__(self, other): def __mul__(self, other): a = self.copy() if isinstance(other, Amount): + assert other["asset"] == self["asset"] a["amount"] *= other["amount"] else: a["amount"] *= other @@ -228,6 +229,7 @@ def __isub__(self, other): def __imul__(self, other): if isinstance(other, Amount): + assert other["asset"] == self["asset"] self["amount"] *= other["amount"] else: self["amount"] *= other diff --git a/bitshares/asset.py b/bitshares/asset.py index ec42f633..a85d5956 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -38,9 +38,9 @@ def __init__( self.full = full super().__init__( asset, - lazy=False, - full=False, - bitshares_instance=None + lazy=lazy, + full=full, + bitshares_instance=bitshares_instance ) def refresh(self): @@ -48,8 +48,8 @@ def refresh(self): """ asset = self.bitshares.rpc.get_asset(self.identifier) if not asset: - raise AssetDoesNotExistsException - super(Asset, self).__init__(asset) + raise AssetDoesNotExistsException(self.identifier) + super(Asset, self).__init__(asset, bitshares_instance=self.bitshares) if self.full: if "bitasset_data_id" in asset: self["bitasset_data"] = self.bitshares.rpc.get_object( @@ -66,6 +66,24 @@ def refresh(self): except: self["description"] = asset["options"]["description"] + @property + def is_fully_loaded(self): + """ Is this instance fully loaded / e.g. all data available? + """ + return ( + self.full and + "bitasset_data_id" in self and + "bitasset_data" in self + ) + + @property + def symbol(self): + return self["symbol"] + + @property + def precision(self): + return self["precision"] + @property def is_bitasset(self): """ Is the asset a :doc:`mpa`? @@ -85,7 +103,7 @@ def flags(self): return self["flags"] def ensure_full(self): - if not self.full: + if not self.is_fully_loaded: self.full = True self.refresh() @@ -97,7 +115,10 @@ def feeds(self): return r = [] for feed in self["bitasset_data"]["feeds"]: - r.append(PriceFeed(feed)) + r.append(PriceFeed( + feed, + bitshares_instance=self.bitshares + )) return r @property @@ -105,11 +126,14 @@ def feed(self): from .price import PriceFeed assert self.is_bitasset self.ensure_full() - return PriceFeed(self["bitasset_data"]["current_feed"]) + return PriceFeed( + self["bitasset_data"]["current_feed"], + bitshares_instance=self.bitshares + ) @property def calls(self): - return self.get_call_positions(10) + return self.get_call_orders(10) def get_call_orders(self, limit=100): from .price import Price @@ -119,10 +143,16 @@ def get_call_orders(self, limit=100): self.ensure_full() r = list() bitasset = self["bitasset_data"] - settlement_price = Price(bitasset["current_feed"]["settlement_price"]) + settlement_price = Price( + bitasset["current_feed"]["settlement_price"], + bitshares_instance=self.bitshares + ) ret = self.bitshares.rpc.get_call_orders(self["id"], limit) for call in ret[:limit]: - call_price = Price(call["call_price"]) + call_price = Price( + call["call_price"], + bitshares_instance=self.bitshares + ) collateral_amount = Amount( { "amount": call["collateral"], @@ -184,7 +214,10 @@ def get_settle_orders(self, limit=100): def halt(self): """ Halt this asset from being moved or traded """ - nullaccount = Account("null-account") # We set the null-account + nullaccount = Account( + "null-account", # We set the null-account + bitshares_instance=self.bitshares + ) flags = {"white_list": True, "transfer_restricted": True, } @@ -464,6 +497,7 @@ def set_market_fee(self, percentage_fee, max_market_fee): :param float percentage_fee: Percentage of fee :param bitshares.amount.Amount max_market_fee: Max Fee + """ assert percentage_fee <= 100 and percentage_fee > 0 flags = {"charge_market_fee": percentage_fee > 0} @@ -487,8 +521,8 @@ def set_market_fee(self, percentage_fee, max_market_fee): def update_feed_producers(self, producers): """ Update bitasset feed producers - :param list producers: List of accounts that are - allowed to produce a feed + :param list producers: List of accounts that are allowed to produce + a feed """ assert self.is_bitasset, \ "Asset needs to be a bitasset/market pegged asset" diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index 7fbd7cc4..b10737d2 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -1,8 +1,5 @@ import json import logging -import random -import re -import collections from datetime import datetime, timedelta from bitsharesapi.bitsharesnoderpc import BitSharesNodeRPC @@ -19,12 +16,10 @@ from .storage import configStorage as config from .exceptions import ( AccountExistsException, - AccountDoesNotExistsException, - InsufficientAuthorityError, - MissingKeyError, ) from .wallet import Wallet -from .transactionbuilder import TransactionBuilder +from .transactionbuilder import TransactionBuilder, ProposalBuilder +from .utils import formatTime, test_proposal_in_buffer log = logging.getLogger(__name__) @@ -35,16 +30,26 @@ class BitShares(object): :param str node: Node to connect to *(optional)* :param str rpcuser: RPC user *(optional)* :param str rpcpassword: RPC password *(optional)* - :param bool nobroadcast: Do **not** broadcast a transaction! *(optional)* + :param bool nobroadcast: Do **not** broadcast a transaction! + *(optional)* :param bool debug: Enable Debugging *(optional)* - :param array,dict,string keys: Predefine the wif keys to shortcut the wallet database *(optional)* - :param bool offline: Boolean to prevent connecting to network (defaults to ``False``) *(optional)* - :param str proposer: Propose a transaction using this proposer *(optional)* - :param int proposal_expiration: Expiration time (in seconds) for the proposal *(optional)* - :param int proposal_review: Review period (in seconds) for the proposal *(optional)* - :param int expiration: Delay in seconds until transactions are supposed to expire *(optional)* - :param str blocking: Wait for broadcasted transactions to be included in a block and return full transaction (can be "head" or "irrversible") - :param bool bundle: Do not broadcast transactions right away, but allow to bundle operations *(optional)* + :param array,dict,string keys: Predefine the wif keys to shortcut the + wallet database *(optional)* + :param bool offline: Boolean to prevent connecting to network (defaults + to ``False``) *(optional)* + :param str proposer: Propose a transaction using this proposer + *(optional)* + :param int proposal_expiration: Expiration time (in seconds) for the + proposal *(optional)* + :param int proposal_review: Review period (in seconds) for the proposal + *(optional)* + :param int expiration: Delay in seconds until transactions are supposed + to expire *(optional)* + :param str blocking: Wait for broadcasted transactions to be included + in a block and return full transaction (can be "head" or + "irrversible") + :param bool bundle: Do not broadcast transactions right away, but allow + to bundle operations *(optional)* Three wallet operation modes are possible: @@ -64,8 +69,8 @@ class BitShares(object): signatures! If no node is provided, it will connect to the node of - http://uptick.rocks. It is **highly** recommended that you pick your own - node instead. Default settings can be changed with: + http://uptick.rocks. It is **highly** recommended that you + pick your own node instead. Default settings can be changed with: .. code-block:: python @@ -84,7 +89,8 @@ class BitShares(object): bitshares = BitShares() print(bitshares.info()) - All that is requires is for the user to have added a key with uptick + All that is requires is for the user to have added a key with + ``uptick`` .. code-block:: bash @@ -120,12 +126,15 @@ def __init__(self, self.nobroadcast = bool(kwargs.get("nobroadcast", False)) self.unsigned = bool(kwargs.get("unsigned", False)) self.expiration = int(kwargs.get("expiration", 30)) - self.proposer = kwargs.get("proposer", None) - self.proposal_expiration = int(kwargs.get("proposal_expiration", 60 * 60 * 24)) - self.proposal_review = int(kwargs.get("proposal_review", 0)) self.bundle = bool(kwargs.get("bundle", False)) self.blocking = kwargs.get("blocking", False) + # Legacy Proposal attributes + self.proposer = kwargs.get("proposer", None) + self.proposal_expiration = int( + kwargs.get("proposal_expiration", 60 * 60 * 24)) + self.proposal_review = int(kwargs.get("proposal_review", 0)) + # Store config for access through other Classes self.config = config @@ -136,8 +145,13 @@ def __init__(self, **kwargs) self.wallet = Wallet(self.rpc, **kwargs) - self.txbuffer = TransactionBuilder(bitshares_instance=self) + # txbuffers/propbuffer are initialized and cleared + self.clear() + + # ------------------------------------------------------------------------- + # Basic Calls + # ------------------------------------------------------------------------- def connect(self, node="", rpcuser="", @@ -159,25 +173,34 @@ def connect(self, self.rpc = BitSharesNodeRPC(node, rpcuser, rpcpassword, **kwargs) - def newWallet(self, pwd): - """ Create a new wallet. This method is basically only calls - :func:`bitshares.wallet.create`. + @property + def prefix(self): + return self.rpc.chain_params["prefix"] - :param str pwd: Password to use for the new wallet - :raises bitshares.exceptions.WalletExists: if there is already a wallet created + def set_default_account(self, account): + """ Set the default account to be used """ - self.wallet.create(pwd) + Account(account) + config["default_account"] = account - def finalizeOp(self, ops, account, permission): + def finalizeOp(self, ops, account, permission, **kwargs): """ This method obtains the required private keys if present in the wallet, finalizes the transaction, signs it and broadacasts it - :param operation ops: The operation (or list of operaions) to broadcast + :param operation ops: The operation (or list of operaions) to + broadcast :param operation account: The account that authorizes the operation :param string permission: The required permission for signing (active, owner, posting) + :param object append_to: This allows to provide an instance of + ProposalsBuilder (see :func:`bitshares.new_proposal`) or + TransactionBuilder (see :func:`bitshares.new_tx()`) to specify + where to put a specific operation. + + ... note:: ``append_to`` is exposed to every method used in the + BitShares class ... note:: @@ -186,10 +209,39 @@ def finalizeOp(self, ops, account, permission): that require active permission with ops that require posting permission. Neither can you use different accounts for different operations! + + ... note:: This uses ``bitshares.txbuffer`` as instance of + :class:`bitshares.transactionbuilder.TransactionBuilder`. + You may want to use your own txbuffer """ - # Append transaction - self.txbuffer.appendOps(ops) + if "append_to" in kwargs and kwargs["append_to"]: + if self.proposer: + log.warn( + "You may not use append_to and bitshares.proposer at " + "the same time. Append bitshares.new_proposal(..) instead" + ) + # Append to the append_to and return + append_to = kwargs["append_to"] + parent = append_to.get_parent() + assert isinstance(append_to, (TransactionBuilder, ProposalBuilder)) + append_to.appendOps(ops) + # Add the signer to the buffer so we sign the tx properly + parent.appendSigner(account, permission) + # This returns as we used append_to, it does NOT broadcast, or sign + return append_to.get_parent() + elif self.proposer: + # Legacy proposer mode! + proposal = self.proposal() + proposal.set_proposer(self.proposer) + proposal.set_expiration(self.proposal_expiration) + proposal.set_review(self.proposal_review) + proposal.appendOps(ops) + # Go forward to see what the other options do ... + else: + # Append tot he default buffer + self.txbuffer.appendOps(ops) + # Add signing information, signer, sign and optionally broadcast if self.unsigned: # In case we don't want to sign anything self.txbuffer.addSigningInformation(account, permission) @@ -238,6 +290,169 @@ def info(self): """ return self.rpc.get_dynamic_global_properties() + # ------------------------------------------------------------------------- + # Wallet stuff + # ------------------------------------------------------------------------- + def newWallet(self, pwd): + """ Create a new wallet. This method is basically only calls + :func:`bitshares.wallet.create`. + + :param str pwd: Password to use for the new wallet + :raises bitshares.exceptions.WalletExists: if there is already a + wallet created + """ + return self.wallet.create(pwd) + + def unlock(self, *args, **kwargs): + """ Unlock the internal wallet + """ + return self.wallet.unlock(*args, **kwargs) + + # ------------------------------------------------------------------------- + # Transaction Buffers + # ------------------------------------------------------------------------- + @property + def txbuffer(self): + """ Returns the currently active tx buffer + """ + return self.tx() + + @property + def propbuffer(self): + """ Return the default proposal buffer + """ + return self.proposal() + + def tx(self): + """ Returns the default transaction buffer + """ + return self._txbuffers[0] + + def proposal( + self, + proposer=None, + proposal_expiration=None, + proposal_review=None + ): + """ Return the default proposal buffer + + ... note:: If any parameter is set, the default proposal + parameters will be changed! + """ + if not self._propbuffer: + return self.new_proposal( + self.tx(), + proposer, + proposal_expiration, + proposal_review + ) + if proposer: + self._propbuffer[0].set_proposer(proposer) + if proposal_expiration: + self._propbuffer[0].set_expiration(proposal_expiration) + if proposal_review: + self._propbuffer[0].set_review(proposal_review) + return self._propbuffer[0] + + def new_proposal( + self, + parent=None, + proposer=None, + proposal_expiration=None, + proposal_review=None + ): + if not parent: + parent = self.tx() + if not proposal_expiration: + proposal_expiration = self.proposal_expiration + + if not proposal_review: + proposal_review = self.proposal_review + + if not proposer: + if "default_account" in config: + proposer = config["default_account"] + + # Else, we create a new object + proposal = ProposalBuilder( + proposer, + proposal_expiration, + proposal_review, + bitshares_instance=self, + parent=parent + ) + if parent: + parent.appendOps(proposal) + self._propbuffer.append(proposal) + return proposal + + def new_tx(self, *args, **kwargs): + """ Let's obtain a new txbuffer + + :returns int txid: id of the new txbuffer + """ + builder = TransactionBuilder( + *args, + bitshares_instance=self, + **kwargs + ) + self._txbuffers.append(builder) + return builder + + def clear(self): + self._txbuffers = [] + self._propbuffer = [] + # Base/Default proposal/tx buffers + 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, bitshares_instance=self) + amount = Amount(amount, asset, bitshares_instance=self) + to = Account(to, bitshares_instance=self) + + memoObj = Memo( + from_account=account, + to_account=to, + bitshares_instance=self + ) + + op = operations.Transfer(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "from": account["id"], + "to": to["id"], + "amount": { + "amount": int(amount), + "asset_id": amount.asset["id"] + }, + "memo": memoObj.encrypt(memo), + "prefix": self.prefix + }) + return self.finalizeOp(op, account, "active", **kwargs) + + # ------------------------------------------------------------------------- + # Account related calls + # ------------------------------------------------------------------------- def create_account( self, account_name, @@ -254,11 +469,12 @@ def create_account( additional_active_accounts=[], proxy_account="proxy-to-self", storekeys=True, + **kwargs ): """ Create new account on BitShares - The brainkey/password can be used to recover all generated keys (see - `bitsharesbase.account` for more details. + The brainkey/password can be used to recover all generated keys + (see `bitsharesbase.account` for more details. By default, this call will use ``default_account`` to register a new name ``account_name`` with all keys being @@ -289,10 +505,14 @@ def create_account( keys will be derived :param array additional_owner_keys: Additional owner public keys :param array additional_active_keys: Additional active public keys - :param array additional_owner_accounts: Additional owner account names - :param array additional_active_accounts: Additional acctive account names - :param bool storekeys: Store new keys in the wallet (default: ``True``) - :raises AccountExistsException: if the account already exists on the blockchain + :param array additional_owner_accounts: Additional owner account + names + :param array additional_active_accounts: Additional acctive account + names + :param bool storekeys: Store new keys in the wallet (default: + ``True``) + :raises AccountExistsException: if the account already exists on + the blockchain """ if not registrar and config["default_account"]: @@ -333,16 +553,19 @@ def create_account( self.wallet.addPrivateKey(active_privkey) self.wallet.addPrivateKey(memo_privkey) elif (owner_key and active_key and memo_key): - active_pubkey = PublicKey(active_key, prefix=self.rpc.chain_params["prefix"]) - owner_pubkey = PublicKey(owner_key, prefix=self.rpc.chain_params["prefix"]) - memo_pubkey = PublicKey(memo_key, prefix=self.rpc.chain_params["prefix"]) + active_pubkey = PublicKey( + active_key, prefix=self.prefix) + owner_pubkey = PublicKey( + owner_key, prefix=self.prefix) + memo_pubkey = PublicKey( + memo_key, prefix=self.prefix) else: raise ValueError( "Call incomplete! Provide either a password or public keys!" ) - owner = format(owner_pubkey, self.rpc.chain_params["prefix"]) - active = format(active_pubkey, self.rpc.chain_params["prefix"]) - memo = format(memo_pubkey, self.rpc.chain_params["prefix"]) + owner = format(owner_pubkey, self.prefix) + active = format(active_pubkey, self.prefix) + memo = format(memo_pubkey, self.prefix) owner_key_authority = [[owner, 1]] active_key_authority = [[active, 1]] @@ -363,7 +586,8 @@ def create_account( active_accounts_authority.append([addaccount["id"], 1]) # voting account - voting_account = Account(proxy_account or "proxy-to-self") + voting_account = Account( + proxy_account or "proxy-to-self", bitshares_instance=self) op = { "fee": {"amount": 0, "asset_id": "1.3.0"}, @@ -387,49 +611,30 @@ def create_account( "extensions": [] }, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix } op = operations.Account_create(**op) - return self.finalizeOp(op, registrar, "active") + return self.finalizeOp(op, registrar, "active", **kwargs) - def transfer(self, to, amount, asset, memo="", account=None): - """ Transfer an asset to another account. + def upgrade_account(self, account=None, **kwargs): + """ Upgrade an account to Lifetime membership - :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`` + :param str account: (optional) the account to allow access + to (defaults to ``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, bitshares_instance=self) - amount = Amount(amount, asset, bitshares_instance=self) - to = Account(to, bitshares_instance=self) - - memoObj = Memo( - from_account=account, - to_account=to, - bitshares_instance=self - ) - - op = operations.Transfer(**{ + op = operations.Account_upgrade(**{ "fee": {"amount": 0, "asset_id": "1.3.0"}, - "from": account["id"], - "to": to["id"], - "amount": { - "amount": int(amount), - "asset_id": amount.asset["id"] - }, - "memo": memoObj.encrypt(memo), - "prefix": self.rpc.chain_params["prefix"] + "account_to_upgrade": account["id"], + "upgrade_to_lifetime_member": True, + "prefix": self.prefix }) - return self.finalizeOp(op, account, "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) def _test_weights_treshold(self, authority): """ This method raises an error if the threshold of an authority cannot @@ -440,14 +645,18 @@ def _test_weights_treshold(self, authority): """ weights = 0 for a in authority["account_auths"]: - weights += a[1] + weights += int(a[1]) for a in authority["key_auths"]: - weights += a[1] + weights += int(a[1]) if authority["weight_threshold"] > weights: raise ValueError("Threshold too restrictive!") + if authority["weight_threshold"] == 0: + raise ValueError("Cannot have threshold of 0") - def allow(self, foreign, weight=None, permission="active", - account=None, threshold=None): + def allow( + self, foreign, weight=None, permission="active", + account=None, threshold=None, **kwargs + ): """ Give additional access to an account by some other public key or account. @@ -481,7 +690,7 @@ def allow(self, foreign, weight=None, permission="active", authority = deepcopy(account[permission]) try: - pubkey = PublicKey(foreign, prefix=self.rpc.chain_params["prefix"]) + pubkey = PublicKey(foreign, prefix=self.prefix) authority["key_auths"].append([ str(pubkey), weight @@ -506,15 +715,17 @@ def allow(self, foreign, weight=None, permission="active", "account": account["id"], permission: authority, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) if permission == "owner": - return self.finalizeOp(op, account["name"], "owner") + return self.finalizeOp(op, account["name"], "owner", **kwargs) else: - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def disallow(self, foreign, permission="active", - account=None, threshold=None): + def disallow( + self, foreign, permission="active", + account=None, threshold=None, **kwargs + ): """ Remove additional access to an account by some other public key or account. @@ -540,7 +751,7 @@ def disallow(self, foreign, permission="active", authority = account[permission] try: - pubkey = PublicKey(foreign, prefix=self.rpc.chain_params["prefix"]) + pubkey = PublicKey(foreign, prefix=self.prefix) affected_items = list( filter(lambda x: x[0] == str(pubkey), authority["key_auths"])) @@ -563,6 +774,8 @@ def disallow(self, foreign, permission="active", "Unknown foreign account or unvalid public key" ) + if not affected_items: + raise ValueError("Changes nothing!") removed_weight = affected_items[0][1] # Define threshold @@ -588,11 +801,11 @@ def disallow(self, foreign, permission="active", "extensions": {} }) if permission == "owner": - return self.finalizeOp(op, account["name"], "owner") + return self.finalizeOp(op, account["name"], "owner", **kwargs) else: - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def update_memo_key(self, key, account=None): + 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 @@ -608,7 +821,7 @@ def update_memo_key(self, key, account=None): if not account: raise ValueError("You need to provide an account") - PublicKey(key, prefix=self.rpc.chain_params["prefix"]) + PublicKey(key, prefix=self.prefix) account = Account(account, bitshares_instance=self) account["options"]["memo_key"] = key @@ -618,9 +831,12 @@ def update_memo_key(self, key, account=None): "new_options": account["options"], "extensions": {} }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def approvewitness(self, witnesses, account=None): + # ------------------------------------------------------------------------- + # Approval and Disapproval of witnesses, workers, committee, and proposals + # ------------------------------------------------------------------------- + def approvewitness(self, witnesses, account=None, **kwargs): """ Approve a witness :param list witnesses: list of Witness name or id @@ -653,11 +869,11 @@ def approvewitness(self, witnesses, account=None): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def disapprovewitness(self, witnesses, account=None): + def disapprovewitness(self, witnesses, account=None, **kwargs): """ Disapprove a witness :param list witnesses: list of Witness name or id @@ -691,11 +907,11 @@ def disapprovewitness(self, witnesses, account=None): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def approvecommittee(self, committees, account=None): + def approvecommittee(self, committees, account=None, **kwargs): """ Approve a committee :param list committees: list of committee member name or id @@ -728,11 +944,11 @@ def approvecommittee(self, committees, account=None): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def disapprovecommittee(self, committees, account=None): + def disapprovecommittee(self, committees, account=None, **kwargs): """ Disapprove a committee :param list committees: list of committee name or id @@ -766,11 +982,97 @@ def disapprovecommittee(self, committees, account=None): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) + + def approveproposal( + self, proposal_ids, account=None, approver=None, **kwargs + ): + """ Approve Proposal - def approveworker(self, workers, account=None): + :param list proposal_id: Ids of the proposals + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + from .proposal import Proposal + 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, bitshares_instance=self) + is_key = approver and approver[:3] == self.prefix + if not approver and not is_key: + approver = account + elif approver and not is_key: + approver = Account(approver, bitshares_instance=self) + else: + approver = PublicKey(approver) + + if not isinstance(proposal_ids, (list, set, tuple)): + proposal_ids = {proposal_ids} + + op = [] + for proposal_id in proposal_ids: + proposal = Proposal(proposal_id, bitshares_instance=self) + update_dict = { + "fee": {"amount": 0, "asset_id": "1.3.0"}, + 'fee_paying_account': account["id"], + 'proposal': proposal["id"], + 'active_approvals_to_add': [approver["id"]], + "prefix": self.prefix + } + if is_key: + update_dict.update({ + 'key_approvals_to_add': [str(approver)], + }) + else: + update_dict.update({ + 'active_approvals_to_add': [approver["id"]], + }) + op.append(operations.Proposal_update(**update_dict)) + if is_key: + self.txbuffer.appendSigner(account["name"], "active") + return self.finalizeOp(op, approver["name"], "active", **kwargs) + + def disapproveproposal( + self, proposal_ids, account=None, approver=None, **kwargs + ): + """ Disapprove Proposal + + :param list proposal_ids: Ids of the proposals + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + from .proposal import Proposal + 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, bitshares_instance=self) + if not approver: + approver = account + else: + approver = Account(approver, bitshares_instance=self) + + if not isinstance(proposal_ids, (list, set, tuple)): + proposal_ids = {proposal_ids} + + op = [] + for proposal_id in proposal_ids: + proposal = Proposal(proposal_id, bitshares_instance=self) + op.append(operations.Proposal_update(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + 'fee_paying_account': account["id"], + 'proposal': proposal["id"], + 'active_approvals_to_remove': [approver["id"]], + "prefix": self.prefix + })) + return self.finalizeOp(op, account["name"], "active", **kwargs) + + def approveworker(self, workers, account=None, **kwargs): """ Approve a worker :param list workers: list of worker member name or id @@ -798,11 +1100,11 @@ def approveworker(self, workers, account=None): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def disapproveworker(self, workers, account=None): + def disapproveworker(self, workers, account=None, **kwargs): """ Disapprove a worker :param list workers: list of worker name or id @@ -831,11 +1133,11 @@ def disapproveworker(self, workers, account=None): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def cancel(self, orderNumbers, account=None): + def cancel(self, orderNumbers, account=None, **kwargs): """ Cancels an order you have placed in a given market. Requires only the "orderNumbers". An order number takes the form ``1.7.xxx``. @@ -860,10 +1162,10 @@ def cancel(self, orderNumbers, account=None): "fee_paying_account": account["id"], "order": order, "extensions": [], - "prefix": self.rpc.chain_params["prefix"]})) - return self.finalizeOp(op, account["name"], "active") + "prefix": self.prefix})) + return self.finalizeOp(op, account["name"], "active", **kwargs) - def vesting_balance_withdraw(self, vesting_id, amount=None, account=None): + def vesting_balance_withdraw(self, vesting_id, amount=None, account=None, **kwargs): """ Withdraw vesting balance :param str vesting_id: Id of the vesting object @@ -891,93 +1193,10 @@ def vesting_balance_withdraw(self, vesting_id, amount=None, account=None): "amount": int(amount), "asset_id": amount["asset"]["id"] }, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active") - def approveproposal(self, proposal_ids, account=None, approver=None): - """ Approve Proposal - - :param list proposal_id: Ids of the proposals - :param str account: (optional) the account to allow access - to (defaults to ``default_account``) - """ - from .proposal import Proposal - 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, bitshares_instance=self) - is_key = approver and approver[:3] == self.rpc.chain_params["prefix"] - if not approver and not is_key: - approver = account - elif approver and not is_key: - approver = Account(approver, bitshares_instance=self) - else: - approver = PublicKey(approver) - - if not isinstance(proposal_ids, (list, set, tuple)): - proposal_ids = {proposal_ids} - - op = [] - for proposal_id in proposal_ids: - proposal = Proposal(proposal_id, bitshares_instance=self) - update_dict = { - "fee": {"amount": 0, "asset_id": "1.3.0"}, - 'fee_paying_account': account["id"], - 'proposal': proposal["id"], - "prefix": self.rpc.chain_params["prefix"] - } - if is_key: - update_dict.update({ - 'key_approvals_to_add': [str(approver)], - }) - else: - update_dict.update({ - 'active_approvals_to_add': [approver["id"]], - }) - op.append(operations.Proposal_update(**update_dict)) - if is_key: - self.txbuffer.appendSigner(account["name"], "active") - return self.finalizeOp(op, approver, "active") - else: - return self.finalizeOp(op, approver["name"], "active") - - def disapproveproposal(self, proposal_ids, account=None, approver=None): - """ Disapprove Proposal - - :param list proposal_ids: Ids of the proposals - :param str account: (optional) the account to allow access - to (defaults to ``default_account``) - """ - from .proposal import Proposal - 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, bitshares_instance=self) - if not approver: - approver = account - else: - approver = Account(approver, bitshares_instance=self) - - if not isinstance(proposal_ids, (list, set, tuple)): - proposal_ids = {proposal_ids} - - op = [] - for proposal_id in proposal_ids: - proposal = Proposal(proposal_id, bitshares_instance=self) - op.append(operations.Proposal_update(**{ - "fee": {"amount": 0, "asset_id": "1.3.0"}, - 'fee_paying_account': account["id"], - 'proposal': proposal["id"], - 'active_approvals_to_remove': [approver["id"]], - "prefix": self.rpc.chain_params["prefix"] - })) - return self.finalizeOp(op, account["name"], "active") - def publish_price_feed( self, symbol, @@ -1001,7 +1220,10 @@ def publish_price_feed( price feed for ``symbol``. For witness produced feeds this means ``account`` is a witness account! """ + assert mcr > 100 + assert mssr > 100 assert isinstance(settlement_price, Price), "settlement_price needs to be instance of `bitshares.price.Price`!" + assert cer is None or isinstance(cer, Price), "cer needs to be instance of `bitshares.price.Price`!" if not account: if "default_account" in config: account = config["default_account"] @@ -1009,59 +1231,44 @@ def publish_price_feed( raise ValueError("You need to provide an account") account = Account(account, bitshares_instance=self) asset = Asset(symbol, bitshares_instance=self, full=True) + backing_asset = asset["bitasset_data"]["options"]["short_backing_asset"] assert asset["id"] == settlement_price["base"]["asset"]["id"] or \ asset["id"] == settlement_price["quote"]["asset"]["id"], \ "Price needs to contain the asset of the symbol you'd like to produce a feed for!" assert asset.is_bitasset, "Symbol needs to be a bitasset!" - assert settlement_price["base"]["asset"]["id"] == asset["bitasset_data"]["options"]["short_backing_asset"] or \ - settlement_price["quote"]["asset"]["id"] == asset["bitasset_data"]["options"]["short_backing_asset"], \ + assert settlement_price["base"]["asset"]["id"] == backing_asset or \ + settlement_price["quote"]["asset"]["id"] == backing_asset, \ "The Price needs to be relative to the backing collateral!" - # Base needs to be short backing asset - if settlement_price["base"]["asset"]["id"] == asset["bitasset_data"]["options"]["short_backing_asset"]: - settlement_price = settlement_price.invert() + settlement_price = settlement_price.as_base(symbol) if cer: - if cer["base"]["asset"]["id"] == asset["bitasset_data"]["options"]["short_backing_asset"]: - cer = cer.invert() + cer = cer.as_base(symbol) + if cer["quote"]["asset"]["id"] != "1.3.0": + raise ValueError( + "CER must be defined against core asset '1.3.0'") else: - cer = settlement_price * 1.05 + if settlement_price["quote"]["asset"]["id"] != "1.3.0": + raise ValueError( + "CER must be manually provided because it relates to core asset '1.3.0'" + ) + cer = settlement_price.as_quote(symbol) * 0.95 op = operations.Asset_publish_feed(**{ "fee": {"amount": 0, "asset_id": "1.3.0"}, "publisher": account["id"], "asset_id": asset["id"], "feed": { - "settlement_price": settlement_price.json(), - "core_exchange_rate": cer.json(), + "settlement_price": settlement_price.as_base(symbol).json(), + "core_exchange_rate": cer.as_base(symbol).json(), "maximum_short_squeeze_ratio": int(mssr * 10), "maintenance_collateral_ratio": int(mcr * 10), }, - "prefix": self.rpc.chain_params["prefix"] - }) - return self.finalizeOp(op, account["name"], "active") - - def upgrade_account(self, account=None): - """ Upgrade an account to Lifetime membership - - :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) - op = operations.Account_upgrade(**{ - "fee": {"amount": 0, "asset_id": "1.3.0"}, - "account_to_upgrade": account["id"], - "upgrade_to_lifetime_member": True, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active") - def update_witness(self, witness_identifier, url=None, key=None): + def update_witness(self, witness_identifier, url=None, key=None, **kwargs): """ Upgrade a witness account :param str witness_identifier: Identifier for the witness @@ -1072,15 +1279,15 @@ def update_witness(self, witness_identifier, url=None, key=None): account = witness.account op = operations.Witness_update(**{ "fee": {"amount": 0, "asset_id": "1.3.0"}, - "prefix": self.rpc.chain_params["prefix"], + "prefix": self.prefix, "witness": witness["id"], "witness_account": account["id"], "new_url": url, "new_signing_key": key, }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def reserve(self, amount, account=None): + def reserve(self, amount, account=None, **kwargs): """ Reserve/Burn an amount of this shares This removes the shares from the supply @@ -1095,7 +1302,7 @@ def reserve(self, amount, account=None): account = config["default_account"] if not account: raise ValueError("You need to provide an account") - account = Account(account) + account = Account(account, bitshares_instance=self) op = operations.Asset_reserve(**{ "fee": {"amount": 0, "asset_id": "1.3.0"}, "payer": account["id"], @@ -1104,7 +1311,7 @@ def reserve(self, amount, account=None): "asset_id": amount["asset"]["id"]}, "extensions": [] }) - return self.finalizeOp(op, account, "active") + return self.finalizeOp(op, account, "active", **kwargs) def create_worker( self, @@ -1115,7 +1322,8 @@ def create_worker( begin=None, payment_type="vesting", pay_vesting_period_days=0, - account=None + account=None, + **kwargs ): """ Reserve/Burn an amount of this shares @@ -1146,7 +1354,7 @@ def create_worker( account = config["default_account"] if not account: raise ValueError("You need to provide an account") - account = Account(account) + account = Account(account, bitshares_instance=self) if payment_type == "refund": initializer = [0, {}] @@ -1169,4 +1377,29 @@ def create_worker( "url": url, "initializer": initializer }) - return self.finalizeOp(op, account, "active") + return self.finalizeOp(op, account, "active", **kwargs) + + def fund_fee_pool(self, symbol, amount, account=None, **kwargs): + """ Fund the fee pool of an asset + + :param str symbol: The symbol to fund the fee pool of + :param float amount: The amount to be burned. + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + assert isinstance(amount, float) + 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, bitshares_instance=self) + asset = Asset(symbol, bitshares_instance=self) + op = operations.Asset_fund_fee_pool(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "from_account": account["id"], + "asset_id": asset["id"], + "amount": int(float(amount) * 10 ** asset["precision"]), + "extensions": [] + }) + return self.finalizeOp(op, account, "active", **kwargs) diff --git a/bitshares/block.py b/bitshares/block.py index 79ea6737..99b5edfe 100644 --- a/bitshares/block.py +++ b/bitshares/block.py @@ -1,14 +1,14 @@ -from bitshares.instance import shared_bitshares_instance -from .blockchainobject import BlockchainObject from .exceptions import BlockDoesNotExistsException from .utils import parse_time +from .blockchainobject import BlockchainObject class Block(BlockchainObject): """ Read a single block from the chain :param int block: block number - :param bitshares.bitshares.BitShares bitshares_instance: BitShares instance + :param bitshares.bitshares.BitShares bitshares_instance: BitShares + instance :param bool lazy: Use lazy loading Instances of this class are dictionaries that come with additional @@ -33,7 +33,26 @@ def refresh(self): block = self.bitshares.rpc.get_block(self.identifier) if not block: raise BlockDoesNotExistsException - super(Block, self).__init__(block) + super(Block, self).__init__(block, bitshares_instance=self.bitshares) + + def time(self): + """ Return a datatime instance for the timestamp of this block + """ + return parse_time(self['timestamp']) + + +class BlockHeader(BlockchainObject): + def refresh(self): + """ Even though blocks never change, you freshly obtain its contents + from an API with this method + """ + block = self.bitshares.rpc.get_block_header(self.identifier) + if not block: + raise BlockDoesNotExistsException + super(BlockHeader, self).__init__( + block, + bitshares_instance=self.bitshares + ) def time(self): """ Return a datatime instance for the timestamp of this block diff --git a/bitshares/blockchain.py b/bitshares/blockchain.py index 1d614f69..61eff3a3 100644 --- a/bitshares/blockchain.py +++ b/bitshares/blockchain.py @@ -1,16 +1,17 @@ import time from .block import Block from bitshares.instance import shared_bitshares_instance -from .utils import parse_time -from bitsharesbase.operationids import operations, getOperationNameForId +from bitsharesbase.operationids import getOperationNameForId class Blockchain(object): """ This class allows to access the blockchain and read data from it - :param bitshares.bitshares.BitShares bitshares_instance: BitShares instance - :param str mode: (default) Irreversible block (``irreversible``) or actual head block (``head``) + :param bitshares.bitshares.BitShares bitshares_instance: BitShares + instance + :param str mode: (default) Irreversible block (``irreversible``) or + actual head block (``head``) This class let's you deal with blockchain related data and methods. """ @@ -71,7 +72,10 @@ def get_current_block(self): .. note:: The block number returned depends on the ``mode`` used when instanciating from this class. """ - return Block(self.get_current_block_num()) + return Block( + self.get_current_block_num(), + bitshares_instance=self.bitshares + ) def block_time(self, block_num): """ Returns a datetime of the block with the given block @@ -79,7 +83,10 @@ def block_time(self, block_num): :param int block_num: Block number """ - return Block(block_num).time() + return Block( + block_num, + bitshares_instance=self.bitshares + ).time() def block_timestamp(self, block_num): """ Returns the timestamp of the block with the given block @@ -87,7 +94,10 @@ def block_timestamp(self, block_num): :param int block_num: Block number """ - return int(Block(block_num).time().timestamp()) + return int(Block( + block_num, + bitshares_instance=self.bitshares + ).time().timestamp()) def blocks(self, start=None, stop=None): """ Yields blocks starting from ``start``. @@ -95,8 +105,8 @@ def blocks(self, start=None, stop=None): :param int start: Starting block :param int stop: Stop at this block :param str mode: We here have the choice between - * "head": the last block - * "irreversible": the block that is confirmed by 2/3 of all block producers and is thus irreversible! + "head" (the last block) and "irreversible" (the block that is + confirmed by 2/3 of all block producers and is thus irreversible) """ # Let's find out how often blocks are generated! block_interval = self.chainParameters().get("block_interval") @@ -108,7 +118,10 @@ def blocks(self, start=None, stop=None): while True: # Get chain properies to identify the - head_block = self.get_current_block_num() + if stop: + head_block = stop + else: + head_block = self.get_current_block_num() # Blocks from start until head block for blocknum in range(start, head_block + 1): @@ -120,19 +133,21 @@ def blocks(self, start=None, stop=None): start = head_block + 1 if stop and start > stop: - raise StopIteration + # raise StopIteration + return # Sleep for one block time.sleep(block_interval) def ops(self, start=None, stop=None, **kwargs): - """ Yields all operations (including virtual operations) starting from ``start``. + """ Yields all operations (including virtual operations) starting from + ``start``. :param int start: Starting block :param int stop: Stop at this block :param str mode: We here have the choice between - * "head": the last block - * "irreversible": the block that is confirmed by 2/3 of all block producers and is thus irreversible! + "head" (the last block) and "irreversible" (the block that is + confirmed by 2/3 of all block producers and is thus irreversible) :param bool only_virtual_ops: Only yield virtual operations This call returns a list that only carries one operation and @@ -157,8 +172,8 @@ def stream(self, opNames=[], *args, **kwargs): :param int start: Start at this block :param int stop: Stop at this block :param str mode: We here have the choice between - * "head": the last block - * "irreversible": the block that is confirmed by 2/3 of all block producers and is thus irreversible! + "head" (the last block) and "irreversible" (the block that is + confirmed by 2/3 of all block producers and is thus irreversible) The dict output is formated such that ``type`` caries the operation type, timestamp and block_num are taken from the @@ -176,7 +191,8 @@ def stream(self, opNames=[], *args, **kwargs): yield r def awaitTxConfirmation(self, transaction, limit=10): - """ Returns the transaction as seen by the blockchain after being included into a block + """ Returns the transaction as seen by the blockchain after being + included into a block .. note:: If you want instant confirmation, you need to instantiate class:`bitshares.blockchain.Blockchain` with @@ -190,15 +206,17 @@ def awaitTxConfirmation(self, transaction, limit=10): transaction contented and thus identifies a transaction uniquely. """ - counter = 0 - start = self.get_current_block_num() - 2 - for block in self.blocks(start=start): + counter = 10 + for block in self.blocks(): counter += 1 for tx in block["transactions"]: - if sorted(tx["signatures"]) == sorted(transaction["signatures"]): + if sorted( + tx["signatures"] + ) == sorted(transaction["signatures"]): return tx if counter > limit: - raise Exception("The operation has not been added after 10 blocks!") + raise Exception( + "The operation has not been added after 10 blocks!") def get_all_accounts(self, start='', stop='', steps=1e3, **kwargs): """ Yields account names between start and stop. diff --git a/bitshares/blockchainobject.py b/bitshares/blockchainobject.py index ce089961..af90fe7e 100644 --- a/bitshares/blockchainobject.py +++ b/bitshares/blockchainobject.py @@ -4,17 +4,21 @@ class ObjectCache(dict): - max_cache_objects = 1000 - - def __init__(self, initial_data={}, max_cache_objects=1000): + def __init__(self, initial_data={}, default_expiration=10): super().__init__(initial_data) - ObjectCache.max_cache_objects = max_cache_objects + self.default_expiration = default_expiration + + def clear(self): + """ Clears the whole cache + """ + dict.__init__(self, dict()) def __setitem__(self, key, value): if key in self: del self[key] data = { - "expires": datetime.utcnow() + timedelta(seconds=10), + "expires": datetime.utcnow() + timedelta( + seconds=self.default_expiration), "data": value } dict.__setitem__(self, key, data) @@ -38,7 +42,8 @@ def __contains__(self, key): return False def __str__(self): - return "ObjectCache(n={}, max_cache_objects={})".format(len(self.keys()), self.max_cache_objects) + return "ObjectCache(n={}, default_expiration={})".format( + len(self.keys()), self.default_expiration) class BlockchainObject(dict): @@ -52,19 +57,24 @@ class BlockchainObject(dict): def __init__( self, data, - *args, klass=None, space_id=1, object_id=None, - lazy=True, + lazy=False, use_cache=True, bitshares_instance=None, + *args, **kwargs ): self.bitshares = bitshares_instance or shared_bitshares_instance() self.cached = False self.identifier = None + # We don't read lists, sets, or tuples + if isinstance(data, (list, set, tuple)): + raise ValueError( + "Cannot interpret lists! Please load elements individually!") + if klass and isinstance(data, klass): self.identifier = data.get("id") super().__init__(data) @@ -82,17 +92,9 @@ def __init__( self.identifier = data else: self.identifier = data - parts = self.identifier.split(".") - if len(parts) == 3: - valid_objectid = False - try: - [int(x) for x in parts] - valid_objectid = True - except: - pass - if valid_objectid: - # Here we assume we deal with an id - self.testid(self.identifier) + if self.test_valid_objectid(self.identifier): + # Here we assume we deal with an id + self.testid(self.identifier) if self.iscached(data): super().__init__(self.getcache(data)) elif not lazy and not self.cached: @@ -102,6 +104,23 @@ def __init__( self.cache() self.cached = True + @staticmethod + def clear_cache(): + if BlockchainObject._cache: + BlockchainObject._cache.clear() + + def test_valid_objectid(self, i): + if "." not in i: + return False + parts = i.split(".") + if len(parts) == 3: + try: + [int(x) for x in parts] + return True + except: + pass + return False + def testid(self, id): parts = id.split(".") if not self.type_id: @@ -111,13 +130,15 @@ def testid(self, id): self.type_ids = [self.type_id] assert int(parts[0]) == self.space_id,\ - "Valid id's for {} are {}.{}.x".format(self.__class__.__name__, self.space_id, self.type_ida) + "Valid id's for {} are {}.{}.x".format( + self.__class__.__name__, self.space_id, self.type_id) assert int(parts[1]) in self.type_ids,\ - "Valid id's for {} are {}.{}.x".format(self.__class__.__name__, self.space_id, self.type_ids) + "Valid id's for {} are {}.{}.x".format( + self.__class__.__name__, self.space_id, self.type_ids) def cache(self): # store in cache - if "id" in self: + if dict.__contains__(self, "id"): BlockchainObject._cache[self.get("id")] = self def iscached(self, id): @@ -142,4 +163,5 @@ def __contains__(self, key): return super().__contains__(key) def __repr__(self): - return "<%s %s>" % (self.__class__.__name__, str(self.identifier)) + return "<%s %s>" % ( + self.__class__.__name__, str(self.identifier)) diff --git a/bitshares/committee.py b/bitshares/committee.py index d214f2b3..c615619c 100644 --- a/bitshares/committee.py +++ b/bitshares/committee.py @@ -1,4 +1,3 @@ -from bitshares.instance import shared_bitshares_instance from .account import Account from .exceptions import CommitteeMemberDoesNotExistsException from .blockchainobject import BlockchainObject @@ -8,20 +7,38 @@ class Committee(BlockchainObject): """ Read data about a Committee Member in the chain :param str member: Name of the Committee Member - :param bitshares bitshares_instance: BitShares() instance to use when accesing a RPC + :param bitshares bitshares_instance: BitShares() instance to use when + accesing a RPC :param bool lazy: Use lazy loading """ type_id = 5 def refresh(self): - account = Account(self.identifier) - member = self.bitshares.rpc.get_committee_member_by_account(account["id"]) + if self.test_valid_objectid(self.identifier): + _, i, _ = self.identifier.split(".") + if int(i) == 2: + account = Account( + self.identifier, bitshares_instance=self.bitshares) + member = self.bitshares.rpc.get_committee_member_by_account( + account["id"]) + elif int(i) == 5: + member = self.bitshares.rpc.get_object(self.identifier) + else: + raise CommitteeMemberDoesNotExistsException + else: + # maybe identifier is an account name + account = Account( + self.identifier, bitshares_instance=self.bitshares) + member = self.bitshares.rpc.get_committee_member_by_account( + account["id"]) + if not member: raise CommitteeMemberDoesNotExistsException - super(Committee, self).__init__(member) - self.cached = True + super(Committee, self).__init__( + member, bitshares_instance=self.bitshares) + self.account_id = account["id"] @property def account(self): - return Account(self.member) + return Account(self.account_id) diff --git a/bitshares/dex.py b/bitshares/dex.py index b5f768f2..ea3376d0 100644 --- a/bitshares/dex.py +++ b/bitshares/dex.py @@ -3,12 +3,8 @@ from .account import Account from .asset import Asset from .amount import Amount -from .market import Market -from .price import Price, Order -from .exceptions import NoWalletException -from .utils import formatTimeFromNow +from .price import Price from bitsharesbase import operations -from bitsharesbase.objects import Operation class Dex(): @@ -84,15 +80,30 @@ def list_debt_positions(self, account=None): r = {} for debt in account.get("call_orders"): - base = Asset(debt["call_price"]["base"]["asset_id"], full=True) - quote = Asset(debt["call_price"]["quote"]["asset_id"], full=True) + base = Asset( + debt["call_price"]["base"]["asset_id"], + full=True, + bitshares_instance=self.bitshares + ) + quote = Asset( + debt["call_price"]["quote"]["asset_id"], + full=True, + bitshares_instance=self.bitshares + ) if not quote.is_bitasset: continue + quote.ensure_full() bitasset = quote["bitasset_data"] - settlement_price = Price(bitasset["current_feed"]["settlement_price"]) + settlement_price = Price( + bitasset["current_feed"]["settlement_price"], + bitshares_instance=self.bitshares + ) if not settlement_price: continue - call_price = Price(debt["call_price"]) + call_price = Price( + debt["call_price"], + bitshares_instance=self.bitshares + ) collateral_amount = Amount({ "amount": debt["collateral"], "asset": base @@ -159,7 +170,11 @@ def adjust_debt(self, delta, new_collateral_ratio=None, account=None): # We sell quote and pay with base symbol = delta["symbol"] - asset = Asset(symbol, full=True) + asset = Asset( + symbol, + full=True, + bitshares_instance=self.bitshares + ) if not asset.is_bitasset: raise ValueError("%s is not a bitasset!" % symbol) bitasset = asset["bitasset_data"] @@ -176,8 +191,14 @@ def adjust_debt(self, delta, new_collateral_ratio=None, account=None): raise ValueError("Collateral Ratio has to be higher than %5.2f" % maintenance_col_ratio) # Derive Amount of Collateral - collateral_asset = Asset(backing_asset_id) - settlement_price = Price(bitasset["current_feed"]["settlement_price"]) + collateral_asset = Asset( + backing_asset_id, + bitshares_instance=self.bitshares + ) + settlement_price = Price( + bitasset["current_feed"]["settlement_price"], + bitshares_instance=self.bitshares + ) if symbol in current_debts: amount_of_collateral = ( diff --git a/bitshares/exceptions.py b/bitshares/exceptions.py index 2b5e8288..d8206507 100644 --- a/bitshares/exceptions.py +++ b/bitshares/exceptions.py @@ -5,6 +5,18 @@ class WalletExists(Exception): pass +class WalletLocked(Exception): + """ Wallet is locked + """ + pass + + +class RPCConnectionRequired(Exception): + """ An RPC connection is required + """ + pass + + class AccountExistsException(Exception): """ The requested account already exists """ @@ -24,68 +36,68 @@ class AssetDoesNotExistsException(Exception): class InvalidAssetException(Exception): - """ The used asset is invalid in this context + """ An invalid asset has been provided """ pass -class BlockDoesNotExistsException(Exception): - """ The block does not exist +class InsufficientAuthorityError(Exception): + """ The transaction requires signature of a higher authority """ pass -class WitnessDoesNotExistsException(Exception): - """ The witness does not exist +class MissingKeyError(Exception): + """ A required key couldn't be found in the wallet """ pass -class CommitteeMemberDoesNotExistsException(Exception): - """ Committee Member does not exist +class InvalidWifError(Exception): + """ The provided private Key has an invalid format """ pass -class VestingBalanceDoesNotExistsException(Exception): - """ Vesting Balance does not exist +class ProposalDoesNotExistException(Exception): + """ The proposal does not exist """ pass -class ProposalDoesNotExistException(Exception): - """ The proposal does not exist +class BlockDoesNotExistsException(Exception): + """ The block does not exist """ pass -class InsufficientAuthorityError(Exception): - """ The transaction requires signature of a higher authority +class NoWalletException(Exception): + """ No Wallet could be found, please use :func:`peerplays.wallet.create` to + create a new wallet """ pass -class MissingKeyError(Exception): - """ A required key couldn't be found in the wallet +class WitnessDoesNotExistsException(Exception): + """ The witness does not exist """ pass -class InvalidWifError(Exception): - """ The provided private Key has an invalid format +class WrongMasterPasswordException(Exception): + """ The password provided could not properly unlock the wallet """ pass -class NoWalletException(Exception): - """ No Wallet could be found, please use :func:`bitshares.wallet.create` to - create a new wallet +class CommitteeMemberDoesNotExistsException(Exception): + """ Committee Member does not exist """ pass -class WrongMasterPasswordException(Exception): - """ The password provided could not properly unlock the wallet +class VestingBalanceDoesNotExistsException(Exception): + """ Vesting Balance does not exist """ pass @@ -94,3 +106,21 @@ class WorkerDoesNotExistsException(Exception): """ Worker does not exist """ pass + + +class ObjectNotInProposalBuffer(Exception): + """ Object was not found in proposal + """ + pass + + +class InvalidMessageSignature(Exception): + """ The message signature does not fit the message + """ + pass + + +class KeyNotFound(Exception): + """ Key not found + """ + pass diff --git a/bitshares/instance.py b/bitshares/instance.py index c604f6c6..72c85a7e 100644 --- a/bitshares/instance.py +++ b/bitshares/instance.py @@ -1,24 +1,33 @@ import bitshares as bts -_shared_bitshares_instance = None + +class SharedInstance(): + instance = None def shared_bitshares_instance(): - """ This method will initialize ``_shared_bitshares_instance`` and return it. + """ This method will initialize ``SharedInstance.instance`` and return it. The purpose of this method is to have offer single default bitshares instance that can be reused by multiple classes. """ - global _shared_bitshares_instance - if not _shared_bitshares_instance: - _shared_bitshares_instance = bts.BitShares() - return _shared_bitshares_instance + if not SharedInstance.instance: + clear_cache() + SharedInstance.instance = bts.BitShares() + return SharedInstance.instance def set_shared_bitshares_instance(bitshares_instance): """ This method allows us to override default bitshares instance for all users of - ``_shared_bitshares_instance``. + ``SharedInstance.instance``. :param bitshares.bitshares.BitShares bitshares_instance: BitShares instance """ - global _shared_bitshares_instance - _shared_bitshares_instance = bitshares_instance + clear_cache() + SharedInstance.instance = bitshares_instance + + +def clear_cache(): + """ Clear Caches + """ + from .blockchainobject import BlockchainObject + BlockchainObject.clear_cache() diff --git a/bitshares/market.py b/bitshares/market.py index c96539f1..c332dc92 100644 --- a/bitshares/market.py +++ b/bitshares/market.py @@ -1,6 +1,7 @@ from bitshares.instance import shared_bitshares_instance from datetime import datetime, timedelta -from .utils import formatTimeFromNow, formatTime +from .utils import ( + formatTimeFromNow, formatTime, formatTimeString, assets_from_string) from .asset import Asset from .amount import Amount from .price import Price, Order, FilledOrder @@ -38,7 +39,6 @@ class Market(dict): quote** and obtain/pay **only base**. """ - market_sep_regex = "[/\-:]" def __init__( self, @@ -51,19 +51,17 @@ def __init__( self.bitshares = bitshares_instance or shared_bitshares_instance() if len(args) == 1 and isinstance(args[0], str): - quote_symbol, base_symbol = self._get_assets_from_string(args[0]) + quote_symbol, base_symbol = assets_from_string(args[0]) quote = Asset(quote_symbol, bitshares_instance=self.bitshares) base = Asset(base_symbol, bitshares_instance=self.bitshares) 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)) - def _get_assets_from_string(self, s): - import re - return re.split(self.market_sep_regex, s) - def get_string(self, separator=":"): """ Return a formated string that identifies the market, e.g. ``USD:BTS`` @@ -73,7 +71,7 @@ def get_string(self, separator=":"): def __eq__(self, other): if isinstance(other, str): - quote_symbol, base_symbol = self._get_assets_from_string(other) + quote_symbol, base_symbol = assets_from_string(other) return ( self["quote"]["symbol"] == quote_symbol and self["base"]["symbol"] == base_symbol @@ -516,6 +514,8 @@ def sell( tx["orderid"] = tx["operation_results"][0][1] self.bitshares.blocking = prevblocking + return tx + def cancel(self, orderNumber, account=None): """ Cancels an order you have placed in a given market. Requires only the "orderNumber". An order number takes the form @@ -534,7 +534,10 @@ def core_quote_market(self): 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"]) + collateral = Asset( + self["quote"]["bitasset_data"]["options"]["short_backing_asset"], + bitshares_instance=self.bitshares + ) return Market(quote=self["quote"], base=collateral) def core_base_market(self): @@ -546,5 +549,8 @@ def core_base_market(self): 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"]) + collateral = Asset( + self["base"]["bitasset_data"]["options"]["short_backing_asset"], + bitshares_instance=self.bitshares + ) return Market(quote=self["base"], base=collateral) diff --git a/bitshares/memo.py b/bitshares/memo.py index 5d74cf14..d7ec659a 100644 --- a/bitshares/memo.py +++ b/bitshares/memo.py @@ -3,7 +3,7 @@ from bitsharesbase import memo as BtsMemo from bitsharesbase.account import PrivateKey, PublicKey from .account import Account -from .exceptions import MissingKeyError +from .exceptions import MissingKeyError, KeyNotFound class Memo(object): @@ -23,24 +23,44 @@ class Memo(object): from bitshares.memo import Memo m = Memo("bitshareseu", "wallet.xeroc") + m.bitshares.wallet.unlock("secret") enc = (m.encrypt("foobar")) print(enc) >> {'nonce': '17329630356955254641', 'message': '8563e2bb2976e0217806d642901a2855'} print(m.decrypt(enc)) >> foobar + To decrypt a memo, simply use + + .. code-block:: python + + from bitshares.memo import Memo + m = Memo() + m.bitshares.wallet.unlock("secret") + print(memo.decrypt(op_data["memo"])) + + if ``op_data`` being the payload of a transfer operation. + """ def __init__( self, - from_account, - to_account, + from_account=None, + to_account=None, bitshares_instance=None ): self.bitshares = bitshares_instance or shared_bitshares_instance() - self.to_account = Account(to_account, bitshares_instance=self.bitshares) - self.from_account = Account(from_account, bitshares_instance=self.bitshares) + if to_account: + self.to_account = Account(to_account, bitshares_instance=self.bitshares) + if from_account: + self.from_account = Account(from_account, bitshares_instance=self.bitshares) + + def unlock_wallet(self, *args, **kwargs): + """ Unlock the library internal wallet + """ + self.bitshares.wallet.unlock(*args, **kwargs) + return self def encrypt(self, memo): """ Encrypt a memo @@ -63,7 +83,7 @@ def encrypt(self, memo): PrivateKey(memo_wif), PublicKey( self.to_account["options"]["memo_key"], - prefix=self.bitshares.rpc.chain_params["prefix"] + prefix=self.bitshares.prefix ), nonce, memo @@ -86,19 +106,29 @@ def decrypt(self, memo): if not memo: return None - memo_wif = self.bitshares.wallet.getPrivateKeyForPublicKey( - self.to_account["options"]["memo_key"] - ) - if not memo_wif: - raise MissingKeyError("Memo key for %s missing!" % self.to_account["name"]) + # We first try to decode assuming we received the memo + try: + memo_wif = self.bitshares.wallet.getPrivateKeyForPublicKey( + memo["to"] + ) + pubkey = memo["from"] + except KeyNotFound: + try: + # if that failed, we assume that we have sent the memo + memo_wif = self.bitshares.wallet.getPrivateKeyForPublicKey( + memo["from"] + ) + pubkey = memo["to"] + except KeyNotFound: + # if all fails, raise exception + raise MissingKeyError( + "Non of the required memo keys are installed!" + "Need any of {}".format( + [memo["to"], memo["from"]])) - # TODO: Use pubkeys of the message, not pubkeys of account! return BtsMemo.decode_memo( PrivateKey(memo_wif), - PublicKey( - self.from_account["options"]["memo_key"], - prefix=self.peerplays.rpc.chain_params["prefix"] - ), + PublicKey(pubkey, prefix=self.bitshares.prefix), memo.get("nonce"), memo.get("message") ) diff --git a/bitshares/message.py b/bitshares/message.py new file mode 100644 index 00000000..21481a1f --- /dev/null +++ b/bitshares/message.py @@ -0,0 +1,140 @@ +import re +import logging +from binascii import hexlify, unhexlify +from graphenebase.ecdsa import verify_message, sign_message +from bitsharesbase.account import PublicKey +from bitshares.instance import shared_bitshares_instance +from bitshares.account import Account +from .exceptions import InvalidMessageSignature +from .storage import configStorage as config + + +log = logging.getLogger(__name__) + +MESSAGE_SPLIT = ( + "-----BEGIN BITSHARES SIGNED MESSAGE-----", + "-----BEGIN META-----", + "-----BEGIN SIGNATURE-----", + "-----END BITSHARES SIGNED MESSAGE-----" +) + +SIGNED_MESSAGE_META = """{message} +account={meta[account]} +memokey={meta[memokey]} +block={meta[block]} +timestamp={meta[timestamp]}""" + +SIGNED_MESSAGE_ENCAPSULATED = """ +{MESSAGE_SPLIT[0]} +{message} +{MESSAGE_SPLIT[1]} +account={meta[account]} +memokey={meta[memokey]} +block={meta[block]} +timestamp={meta[timestamp]} +{MESSAGE_SPLIT[2]} +{signature} +{MESSAGE_SPLIT[3]} +""" + + +class Message(): + + def __init__(self, message, bitshares_instance=None): + self.bitshares = bitshares_instance or shared_bitshares_instance() + self.message = message + + def sign(self, account=None, **kwargs): + """ Sign a message with an account's memo key + + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + + :returns: the signed message encapsulated in a known format + """ + if not account: + if "default_account" in config: + account = config["default_account"] + if not account: + raise ValueError("You need to provide an account") + + # Data for message + account = Account(account, bitshares_instance=self.bitshares) + info = self.bitshares.info() + meta = dict( + timestamp=info["time"], + block=info["head_block_number"], + memokey=account["options"]["memo_key"], + account=account["name"]) + + # wif key + wif = self.bitshares.wallet.getPrivateKeyForPublicKey( + account["options"]["memo_key"] + ) + + # signature + message = self.message.strip() + signature = hexlify(sign_message( + SIGNED_MESSAGE_META.format(**locals()), + wif + )).decode("ascii") + + message = self.message + return SIGNED_MESSAGE_ENCAPSULATED.format( + MESSAGE_SPLIT=MESSAGE_SPLIT, + **locals() + ) + + def verify(self, **kwargs): + """ Verify a message with an account's memo key + + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + + :returns: True if the message is verified successfully + :raises InvalidMessageSignature if the signature is not ok + """ + # Split message into its parts + parts = re.split("|".join(MESSAGE_SPLIT), self.message) + parts = [x for x in parts if x.strip()] + + assert len(parts) > 2, "Incorrect number of message parts" + + message = parts[0].strip() + signature = parts[2].strip() + # Parse the meta data + meta = dict(re.findall(r'(\S+)=(.*)', parts[1])) + + # Ensure we have all the data in meta + assert "account" in meta + assert "memokey" in meta + assert "block" in meta + assert "timestamp" in meta + + # Load account from blockchain + account = Account( + meta.get("account"), + bitshares_instance=self.bitshares) + + # Test if memo key is the same as on the blockchain + if not account["options"]["memo_key"] == meta["memokey"]: + log.error( + "Memo Key of account {} on the Blockchain".format( + account["name"]) + + "differs from memo key in the message: {} != {}".format( + account["options"]["memo_key"], meta["memokey"] + ) + ) + + # Reformat message + message = SIGNED_MESSAGE_META.format(**locals()) + + # Verify Signature + pubkey = verify_message(message, unhexlify(signature)) + + # Verify pubky + pk = PublicKey(hexlify(pubkey).decode("ascii")) + if format(pk, self.bitshares.prefix) != meta["memokey"]: + raise InvalidMessageSignature + + return True diff --git a/bitshares/notify.py b/bitshares/notify.py index 9eff9de8..bbeb528e 100644 --- a/bitshares/notify.py +++ b/bitshares/notify.py @@ -4,7 +4,7 @@ from bitshares.instance import shared_bitshares_instance from bitshares.market import Market from bitshares.price import Order, FilledOrder, UpdateCallOrder -from bitshares.account import Account, AccountUpdate +from bitshares.account import AccountUpdate log = logging.getLogger(__name__) # logging.basicConfig(level=logging.DEBUG) @@ -125,7 +125,10 @@ def process_market(self, data): if isinstance(d, str): # Single order has been placed log.debug("Calling on_market with Order()") - self.on_market(Order(d)) + self.on_market(Order( + d, + bitshares_instance=self.bitshares + )) continue elif isinstance(d, dict): d = [d] @@ -137,11 +140,20 @@ def process_market(self, data): for i in p: if isinstance(i, dict): if "pays" in i and "receives" in i: - self.on_market(FilledOrder(i)) + self.on_market(FilledOrder( + i, + bitshares_instance=self.bitshares + )) elif "for_sale" in i and "sell_price" in i: - self.on_market(Order(i)) + self.on_market(Order( + i, + bitshares_instance=self.bitshares + )) elif "collateral" in i and "call_price" in i: - self.on_market(UpdateCallOrder(i)) + self.on_market(UpdateCallOrder( + i, + bitshares_instance=self.bitshares + )) else: if i: log.error( @@ -152,7 +164,10 @@ def process_account(self, message): """ This is used for processing of account Updates. It will return instances of :class:bitshares.account.AccountUpdate` """ - self.on_account(AccountUpdate(message)) + self.on_account(AccountUpdate( + message, + bitshares_instance=self.bitshares + )) def listen(self): """ This call initiates the listening/notification process. It diff --git a/bitshares/price.py b/bitshares/price.py index 5e4938f6..f66ff37b 100644 --- a/bitshares/price.py +++ b/bitshares/price.py @@ -5,8 +5,7 @@ from .amount import Amount from .asset import Asset from .utils import formatTimeString -from .witness import Witness -from .utils import parse_time +from .utils import parse_time, assets_from_string class Price(dict): @@ -77,7 +76,7 @@ def __init__( if (len(args) == 1 and isinstance(args[0], str) and not base and not quote): import re price, assets = args[0].split(" ") - base_symbol, quote_symbol = re.split("[/-:]", assets) + base_symbol, quote_symbol = assets_from_string(assets) base = Asset(base_symbol, bitshares_instance=self.bitshares) quote = Asset(quote_symbol, bitshares_instance=self.bitshares) frac = Fraction(float(price)).limit_denominator(10 ** base["precision"]) @@ -100,7 +99,7 @@ def __init__( elif len(args) == 1 and isinstance(args[0], dict) and "receives" in args[0]: # Filled order assert base_asset, "Need a 'base_asset' asset" - base_asset = Asset(base_asset) + base_asset = Asset(base_asset, bitshares_instance=self.bitshares) if args[0]["receives"]["asset_id"] == base_asset["id"]: # If the seller received "base" in a quote_base market, than # it has been a sell order of quote @@ -121,8 +120,8 @@ def __init__( elif (len(args) == 1 and isinstance(base, str) and isinstance(quote, str)): price = args[0] - base = Asset(base) - quote = Asset(quote) + base = Asset(base, bitshares_instance=self.bitshares) + quote = Asset(quote, bitshares_instance=self.bitshares) frac = Fraction(float(price)).limit_denominator(10 ** base["precision"]) self["quote"] = Amount(amount=frac.denominator, asset=quote, bitshares_instance=self.bitshares) self["base"] = Amount(amount=frac.numerator, asset=base, bitshares_instance=self.bitshares) @@ -149,7 +148,7 @@ def __init__( isinstance(args[1], str)): import re price = args[0] - base_symbol, quote_symbol = re.split("[/-:]", args[1]) + base_symbol, quote_symbol = assets_from_string(args[1]) base = Asset(base_symbol, bitshares_instance=self.bitshares) quote = Asset(quote_symbol, bitshares_instance=self.bitshares) frac = Fraction(float(price)).limit_denominator(10 ** base["precision"]) @@ -238,14 +237,36 @@ def __mul__(self, other): a = self.copy() if isinstance(other, Price): # Rotate/invert other - if self["quote"]["symbol"] in other.symbols(): - other = other.as_quote(self["quote"]["symbol"]) - elif self["base"]["symbol"] in other.symbols(): - other = other.as_quote(self["base"]["symbol"]) - else: + if ( + self["quote"]["symbol"] not in other.symbols() and + self["base"]["symbol"] not in other.symbols() + ): raise InvalidAssetException - a["base"] = Amount(float(self["base"] * other["base"]), other["base"]["symbol"]) - a["quote"] = Amount(float(self["quote"] * other["quote"]), self["quote"]["symbol"]) + + # base/quote = a/b + # a/b * b/c = a/c + a = self.copy() + if self["quote"]["symbol"] == other["base"]["symbol"]: + a["base"] = Amount( + float(self["base"]) * float(other["base"]), self["base"]["symbol"], + bitshares_instance=self.bitshares + ) + a["quote"] = Amount( + float(self["quote"]) * float(other["quote"]), other["quote"]["symbol"], + bitshares_instance=self.bitshares + ) + # a/b * c/a = c/b + elif self["base"]["symbol"] == other["quote"]["symbol"]: + a["base"] = Amount( + float(self["base"]) * float(other["base"]), other["base"]["symbol"], + bitshares_instance=self.bitshares + ) + a["quote"] = Amount( + float(self["quote"]) * float(other["quote"]), self["quote"]["symbol"], + bitshares_instance=self.bitshares + ) + else: + raise ValueError("Wrong rotation of prices") elif isinstance(other, Amount): assert other["asset"]["id"] == self["quote"]["asset"]["id"] a = other.copy() * self["price"] @@ -276,8 +297,14 @@ def __div__(self, other): other = other.as_base(self["base"]["symbol"]) else: raise InvalidAssetException - a["base"] = Amount(float(self["quote"] / other["quote"]), other["quote"]["symbol"]) - a["quote"] = Amount(float(self["base"] / other["base"]), self["quote"]["symbol"]) + a["base"] = Amount( + float(self["quote"] / other["quote"]), other["quote"]["symbol"], + bitshares_instance=self.bitshares + ) + a["quote"] = Amount( + float(self["base"] / other["base"]), self["quote"]["symbol"], + bitshares_instance=self.bitshares + ) elif isinstance(other, Amount): assert other["asset"]["id"] == self["quote"]["asset"]["id"] a = other.copy() / self["price"] @@ -383,7 +410,6 @@ class Order(Price): 'deleted' key which is set to ``True`` and all other data be ``None``. """ - def __init__(self, *args, bitshares_instance=None, **kwargs): self.bitshares = bitshares_instance or shared_bitshares_instance() @@ -467,6 +493,8 @@ def __init__(self, order, bitshares_instance=None, **kwargs): quote=kwargs.get("quote"), ) self["time"] = formatTimeString(order["date"]) + self["side1_account_id"] = order["side1_account_id"] + self["side2_account_id"] = order["side2_account_id"] elif isinstance(order, dict): # filled orders from account history @@ -550,7 +578,11 @@ def __init__(self, feed, bitshares_instance=None): self.bitshares = bitshares_instance or shared_bitshares_instance() if len(feed) == 2: super(PriceFeed, self).__init__({ - "producer": Account(feed[0], lazy=True), + "producer": Account( + feed[0], + lazy=True, + bitshares_instance=self.bitshares + ), "date": parse_time(feed[1][0]), "maintenance_collateral_ratio": feed[1][1]["maintenance_collateral_ratio"], "maximum_short_squeeze_ratio": feed[1][1]["maximum_short_squeeze_ratio"], diff --git a/bitshares/proposal.py b/bitshares/proposal.py index 186e0c21..07471dc7 100644 --- a/bitshares/proposal.py +++ b/bitshares/proposal.py @@ -19,7 +19,11 @@ def refresh(self): proposal = self.bitshares.rpc.get_objects([self.identifier]) if not any(proposal): raise ProposalDoesNotExistException - super(Proposal, self).__init__(proposal[0]) + super(Proposal, self).__init__(proposal[0], bitshares_instance=self.bitshares) + + @property + def proposed_operations(self): + yield from self["proposed_transaction"]["operations"] class Proposals(list): @@ -31,7 +35,7 @@ class Proposals(list): def __init__(self, account, bitshares_instance=None): self.bitshares = bitshares_instance or shared_bitshares_instance() - account = Account(account) + account = Account(account, bitshares_instance=self.bitshares) proposals = self.bitshares.rpc.get_proposed_transactions(account["id"]) super(Proposals, self).__init__( diff --git a/bitshares/transactionbuilder.py b/bitshares/transactionbuilder.py index c6f51e88..7763862d 100644 --- a/bitshares/transactionbuilder.py +++ b/bitshares/transactionbuilder.py @@ -1,5 +1,4 @@ from .account import Account -from .blockchain import Blockchain from bitsharesbase.objects import Operation from bitsharesbase.account import PrivateKey, PublicKey from bitsharesbase.signedtransactions import Signed_Transaction @@ -7,26 +6,187 @@ from .exceptions import ( InsufficientAuthorityError, MissingKeyError, - InvalidWifError + InvalidWifError, + WalletLocked ) from bitshares.instance import shared_bitshares_instance import logging log = logging.getLogger(__name__) +class ProposalBuilder: + """ Proposal Builder allows us to construct an independent Proposal + that may later be added to an instance ot TransactionBuilder + + :param str proposer: Account name of the proposing user + :param int proposal_expiration: Number seconds until the proposal is + supposed to expire + :param int proposal_review: Number of seconds for review of the + proposal + :param bitshares.transactionbuilder.TransactionBuilder: Specify + your own instance of transaction builder (optional) + :param bitshares.bitshares.BitShares bitshares_instance: BitShares + instance + """ + def __init__( + self, + proposer, + proposal_expiration=None, + proposal_review=None, + parent=None, + bitshares_instance=None, + *args, + **kwargs + ): + self.bitshares = bitshares_instance or shared_bitshares_instance() + + self.set_expiration(proposal_expiration or 2 * 24 * 60 * 60) + self.set_review(proposal_review) + self.set_parent(parent) + self.set_proposer(proposer) + self.ops = list() + + def is_empty(self): + return not (len(self.ops) > 0) + + def set_proposer(self, p): + self.proposer = p + + def set_expiration(self, p): + self.proposal_expiration = p + + def set_review(self, p): + self.proposal_review = p + + def set_parent(self, p): + self.parent = p + + def appendOps(self, ops, append_to=None): + """ Append op(s) to the transaction builder + + :param list ops: One or a list of operations + """ + if isinstance(ops, list): + self.ops.extend(ops) + else: + self.ops.append(ops) + parent = self.parent + if parent: + parent._set_require_reconstruction() + + def list_operations(self): + return [Operation(o) for o in self.ops] + + def broadcast(self): + assert self.parent, "No parent transaction provided!" + self.parent._set_require_reconstruction() + return self.parent.broadcast() + + def get_parent(self): + """ This allows to referr to the actual parent of the Proposal + """ + return self.parent + + def __repr__(self): + return "" % str(self.ops) + + def json(self): + """ Return the json formated version of this proposal + """ + raw = self.get_raw() + if not raw: + return dict() + return raw.json() + + def get_raw(self): + """ Returns an instance of base "Operations" for further processing + """ + if not self.ops: + return + ops = [operations.Op_wrapper(op=o) for o in list(self.ops)] + proposer = Account( + self.proposer, + bitshares_instance=self.bitshares + ) + data = { + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "fee_paying_account": proposer["id"], + "expiration_time": transactions.formatTimeFromNow( + self.proposal_expiration), + "proposed_ops": [o.json() for o in ops], + "extensions": [] + } + if self.proposal_review: + data.update({ + "review_period_seconds": self.proposal_review + }) + ops = operations.Proposal_create(**data) + return Operation(ops) + + class TransactionBuilder(dict): """ This class simplifies the creation of transactions by adding operations and signers. """ - - def __init__(self, tx={}, bitshares_instance=None): + def __init__( + self, + tx={}, + proposer=None, + bitshares_instance=None + ): self.bitshares = bitshares_instance or shared_bitshares_instance() self.clear() if not isinstance(tx, dict): raise ValueError("Invalid TransactionBuilder Format") super(TransactionBuilder, self).__init__(tx) + # Do we need to reconstruct the tx from self.ops? + self._require_reconstruction = True + + def is_empty(self): + return not (len(self.ops) > 0) + + def list_operations(self): + return [Operation(o) for o in self.ops] + + def _is_signed(self): + return "signatures" in self and self["signatures"] + + def _is_constructed(self): + return "expiration" in self and self["expiration"] + + def _is_require_reconstruction(self): + return self._require_reconstruction + + def _set_require_reconstruction(self): + self._require_reconstruction = True - def appendOps(self, ops): + def _unset_require_reconstruction(self): + self._require_reconstruction = False + + def __repr__(self): + return str(self) + + def __str__(self): + return str(self.json()) + + def __getitem__(self, key): + if key not in self: + self.constructTx() + return dict(self).__getitem__(key) + + def get_parent(self): + """ TransactionBuilders don't have parents, they are their own parent + """ + return self + + def json(self): + """ Show the transaction as plain json + """ + if not self._is_constructed() or self._is_require_reconstruction(): + self.constructTx() + return dict(self) + + def appendOps(self, ops, append_to=None): """ Append op(s) to the transaction builder :param list ops: One or a list of operations @@ -35,34 +195,44 @@ def appendOps(self, ops): self.ops.extend(ops) else: self.ops.append(ops) + self._set_require_reconstruction() def appendSigner(self, account, permission): """ Try to obtain the wif key from the wallet by telling which account and permission is supposed to sign the transaction """ + assert permission in ["active", "owner"], "Invalid permission" + account = Account(account, bitshares_instance=self.bitshares) + required_treshold = account[permission]["weight_threshold"] + + if self.bitshares.wallet.locked(): + raise WalletLocked() + def fetchkeys(account, perm, level=0): if level > 2: return [] r = [] for authority in account[perm]["key_auths"]: - wif = self.bitshares.wallet.getPrivateKeyForPublicKey(authority[0]) - if wif: + try: + wif = self.bitshares.wallet.getPrivateKeyForPublicKey( + authority[0]) r.append([wif, authority[1]]) + except Exception: + pass if sum([x[1] for x in r]) < required_treshold: # go one level deeper for authority in account[perm]["account_auths"]: - auth_account = Account(authority[0], bitshares_instance=self.bitshares) + auth_account = Account( + authority[0], bitshares_instance=self.bitshares) r.extend(fetchkeys(auth_account, perm, level + 1)) return r - assert permission in ["active", "owner"], "Invalid permission" - - if account not in self.available_signers: + if account not in self.signing_accounts: # is the account an instance of public key? if isinstance(account, PublicKey): - self.wifs.append( + self.wifs.add( self.bitshares.wallet.getPrivateKeyForPublicKey( str(account) ) @@ -73,9 +243,10 @@ def fetchkeys(account, perm, level=0): keys = fetchkeys(account, permission) if permission != "owner": keys.extend(fetchkeys(account, "owner")) - self.wifs.extend([x[0] for x in keys]) + for x in keys: + self.wifs.add(x[0]) - self.available_signers.append(account) + self.signing_accounts.append(account) def appendWif(self, wif): """ Add a wif that should be used for signing of the transaction. @@ -83,7 +254,7 @@ def appendWif(self, wif): if wif: try: PrivateKey(wif) - self.wifs.append(wif) + self.wifs.add(wif) except: raise InvalidWifError @@ -91,35 +262,31 @@ def constructTx(self): """ Construct the actual transaction and store it in the class's dict store """ - if self.bitshares.proposer: - ops = [operations.Op_wrapper(op=o) for o in list(self.ops)] - proposer = Account( - self.bitshares.proposer, - bitshares_instance=self.bitshares - ) - ops = operations.Proposal_create(**{ - "fee": {"amount": 0, "asset_id": "1.3.0"}, - "fee_paying_account": proposer["id"], - "expiration_time": transactions.formatTimeFromNow( - self.bitshares.proposal_expiration), - "proposed_ops": [o.json() for o in ops], - "review_period_seconds": self.bitshares.proposal_review, - "extensions": [] - }) - ops = [Operation(ops)] - else: - ops = [Operation(o) for o in list(self.ops)] + ops = list() + for op in self.ops: + if isinstance(op, ProposalBuilder): + # This operation is a proposal an needs to be deal with + # differently + proposals = op.get_raw() + if proposals: + ops.append(proposals) + else: + # otherwise, we simply wrap ops into Operations + ops.extend([Operation(op)]) + # We no wrap everything into an actual transaction ops = transactions.addRequiredFees(self.bitshares.rpc, ops) expiration = transactions.formatTimeFromNow(self.bitshares.expiration) - ref_block_num, ref_block_prefix = transactions.getBlockParams(self.bitshares.rpc) - tx = Signed_Transaction( + ref_block_num, ref_block_prefix = transactions.getBlockParams( + self.bitshares.rpc) + self.tx = Signed_Transaction( ref_block_num=ref_block_num, ref_block_prefix=ref_block_prefix, expiration=expiration, operations=ops ) - super(TransactionBuilder, self).__init__(tx.json()) + super(TransactionBuilder, self).__init__(self.tx.json()) + self._unset_require_reconstruction() def sign(self): """ Sign a provided transaction witht he provided key(s) @@ -132,18 +299,24 @@ def sign(self): """ self.constructTx() + if "operations" not in self or not self["operations"]: + return + + # Legacy compatibility! # If we are doing a proposal, obtain the account from the proposer_id if self.bitshares.proposer: proposer = Account( self.bitshares.proposer, bitshares_instance=self.bitshares) - self.wifs = [] + self.wifs = set() + self.signing_accounts = list() self.appendSigner(proposer["id"], "active") # We need to set the default prefix, otherwise pubkeys are # presented wrongly! if self.bitshares.rpc: - operations.default_prefix = self.bitshares.rpc.chain_params["prefix"] + operations.default_prefix = ( + self.bitshares.rpc.chain_params["prefix"]) elif "blockchain" in self: operations.default_prefix = self["blockchain"]["prefix"] @@ -168,51 +341,47 @@ def verify_authority(self): raise e def broadcast(self): - """ Broadcast a transaction to the BitShares network + """ Broadcast a transaction to the bitshares network :param tx tx: Signed transaction to broadcast """ - if "signatures" not in self or not self["signatures"]: + # Cannot broadcast an empty transaction + if not self._is_signed(): self.sign() + if "operations" not in self or not self["operations"]: + return + + ret = self.json() + if self.bitshares.nobroadcast: log.warning("Not broadcasting anything!") - return self + self.clear() + return ret - tx = self.json() # Broadcast - # FIXME: broadcast_transaction_synchronous + try: + if self.bitshares.blocking: + ret = self.bitshares.rpc.broadcast_transaction_synchronous( + ret, api="network_broadcast") + ret.update(**ret["trx"]) + else: + self.bitshares.rpc.broadcast_transaction( + ret, api="network_broadcast") + except Exception as e: + raise e - if self.bitshares.blocking: - try: - tx = self.bitshares.rpc.broadcast_transaction_synchronous(tx, api="network_broadcast") - except Exception as e: - raise e - else: - try: - self.bitshares.rpc.broadcast_transaction(tx, api="network_broadcast") - except Exception as e: - raise e - - return tx - - """ # Legacy code for blocking - if self.bitshares.blocking: - chain = Blockchain( - mode=("head" if self.bitshares.blocking == "head" else "irreversible"), - bitshares_instance=self.bitshares - ) - tx = chain.awaitTxConfirmation(tx) - return tx - """ + self.clear() + return ret def clear(self): """ Clear the transaction builder and start from scratch """ self.ops = [] - self.wifs = [] - self.pop("signatures", None) - self.available_signers = [] + self.wifs = set() + self.signing_accounts = [] + # This makes sure that _is_constructed will return False afterwards + self["expiration"] = None super(TransactionBuilder, self).__init__({}) def addSigningInformation(self, account, permission): @@ -255,11 +424,6 @@ def addSigningInformation(self, account, permission): [x[0] for x in account_auth_account[permission]["key_auths"]] ) - def json(self): - """ Show the transaction as plain json - """ - return dict(self) - def appendMissingSignatures(self): """ Store which accounts/keys are supposed to sign the transaction diff --git a/bitshares/utils.py b/bitshares/utils.py index 0546a4a0..762a76fa 100644 --- a/bitshares/utils.py +++ b/bitshares/utils.py @@ -1,5 +1,7 @@ +import re import time from datetime import datetime +from .exceptions import ObjectNotInProposalBuffer timeFormat = '%Y-%m-%dT%H:%M:%S' @@ -28,10 +30,47 @@ def formatTimeFromNow(secs=0): :rtype: str """ - return datetime.utcfromtimestamp(time.time() + int(secs)).strftime(timeFormat) + return datetime.utcfromtimestamp( + time.time() + int(secs)).strftime(timeFormat) def parse_time(block_time): - """Take a string representation of time from the blockchain, and parse it into datetime object. + """Take a string representation of time from the blockchain, and parse it + into datetime object. """ return datetime.strptime(block_time, timeFormat) + + +def assets_from_string(text): + """Correctly split a string containing an asset pair. + + Splits the string into two assets with the separator being on of the + following: ``:``, ``/``, or ``-``. + """ + return re.split(r'[\-:/]', text) + + +def test_proposal_in_buffer(buf, operation_name, id): + from .transactionbuilder import ProposalBuilder + from peerplaysbase.operationids import operations + assert isinstance(buf, ProposalBuilder) + + operationid = operations.get(operation_name) + _, _, j = id.split(".") + + ops = buf.list_operations() + if (len(ops) <= int(j)): + raise ObjectNotInProposalBuffer( + "{} with id {} not found".format( + operation_name, + id + ) + ) + op = ops[int(j)].json() + if op[0] != operationid: + raise ObjectNotInProposalBuffer( + "{} with id {} not found".format( + operation_name, + id + ) + ) diff --git a/bitshares/vesting.py b/bitshares/vesting.py index 9721495f..f66abef6 100644 --- a/bitshares/vesting.py +++ b/bitshares/vesting.py @@ -1,4 +1,3 @@ -from .instance import shared_bitshares_instance from .account import Account from .exceptions import VestingBalanceDoesNotExistsException from .blockchainobject import BlockchainObject @@ -17,11 +16,11 @@ def refresh(self): obj = self.bitshares.rpc.get_objects([self.identifier])[0] if not obj: raise VestingBalanceDoesNotExistsException - super(Vesting, self).__init__(obj) + super(Vesting, self).__init__(obj, bitshares_instance=self.bitshares) @property def account(self): - return Account(self["owner"]) + return Account(self["owner"], bitshares_instance=self.bitshares) @property def claimable(self): @@ -33,7 +32,10 @@ def claimable(self): float(self["balance"]["amount"])) / float(p["vesting_seconds"]) ) if float(p["vesting_seconds"]) > 0.0 else 1 - return Amount(self["balance"]) * ratio + return Amount( + self["balance"], + bitshares_instance=self.bitshares + ) * ratio else: raise NotImplementedError("This policy isn't implemented yet") diff --git a/bitshares/wallet.py b/bitshares/wallet.py index c9b6fbfb..2e2b5fc9 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -1,15 +1,16 @@ import logging import os - from graphenebase import bip38 from bitsharesbase.account import PrivateKey, GPHPrivateKey - from .account import Account from .exceptions import ( + KeyNotFound, InvalidWifError, WalletExists, + WalletLocked, WrongMasterPasswordException, - NoWalletException + NoWalletException, + RPCConnectionRequired ) log = logging.getLogger(__name__) @@ -21,7 +22,8 @@ class Wallet(): or uses a SQLite database managed by storage.py. :param BitSharesNodeRPC rpc: RPC connection to a BitShares node - :param array,dict,string keys: Predefine the wif keys to shortcut the wallet database + :param array,dict,string keys: Predefine the wif keys to shortcut the + wallet database Three wallet operation modes are possible: @@ -53,12 +55,13 @@ class Wallet(): keys = {} # struct with pubkey as key and wif as value keyMap = {} # type:wif pairs to force certain keys - def __init__(self, rpc, *args, **kwargs): + def __init__(self, rpc=None, *args, **kwargs): from .storage import configStorage self.configStorage = configStorage - # RPC - Wallet.rpc = rpc + # RPC static variable + if rpc: + Wallet.rpc = rpc # Prefix? if Wallet.rpc: @@ -86,7 +89,8 @@ def setKeys(self, loadkeys): """ This method is strictly only for in memory keys that are passed to Wallet/BitShares with the ``keys`` argument """ - log.debug("Force setting of private keys. Not using the wallet database!") + log.debug( + "Force setting of private keys. Not using the wallet database!") if isinstance(loadkeys, dict): Wallet.keyMap = loadkeys loadkeys = list(loadkeys.values()) @@ -126,9 +130,16 @@ def lock(self): """ self.masterpassword = None + def unlocked(self): + """ Is the wallet database unlocked? + """ + return not self.locked() + def locked(self): """ Is the wallet database locked? """ + if Wallet.keys: # Keys have been manually provided! + return False try: self.tryUnlockFromEnv() except: @@ -165,12 +176,14 @@ def newWallet(self, pwd): raise WalletExists("You already have created a wallet!") self.masterpwd = self.MasterPassword(pwd) self.masterpassword = self.masterpwd.decrypted_master + self.masterpwd.saveEncrytpedMaster() def encrypt_wif(self, wif): """ Encrypt a wif key """ assert not self.locked() - return format(bip38.encrypt(PrivateKey(wif), self.masterpassword), "encwif") + return format( + bip38.encrypt(PrivateKey(wif), self.masterpassword), "encwif") def decrypt_wif(self, encwif): """ decrypt a wif key @@ -187,13 +200,15 @@ def decrypt_wif(self, encwif): def addPrivateKey(self, wif): """ Add a private key to the wallet database """ - # it could be either graphenebase or bitsharesbase so we can't check the type directly + # it could be either graphenebase or peerplaysbase so we can't check + # the type directly if isinstance(wif, PrivateKey) or isinstance(wif, GPHPrivateKey): wif = str(wif) try: pub = format(PrivateKey(wif).pubkey, self.prefix) except: - raise InvalidWifError("Invalid Private Key Format. Please use WIF!") + raise InvalidWifError( + "Invalid Private Key Format. Please use WIF!") if self.keyStorage: # Test if wallet exists @@ -219,7 +234,13 @@ def getPrivateKeyForPublicKey(self, pub): if not self.created(): raise NoWalletException - return self.decrypt_wif(self.keyStorage.getPrivateKeyForPublicKey(pub)) + if not self.unlocked(): + raise WalletLocked + + encwif = self.keyStorage.getPrivateKeyForPublicKey(pub) + if not encwif: + raise KeyNotFound("No private key for {} found".format(pub)) + return self.decrypt_wif(encwif) def removePrivateKeyFromPublicKey(self, pub): """ Remove a key from the wallet database @@ -262,7 +283,8 @@ def getMemoKeyForAccount(self, name): account = self.rpc.get_account(name) if not account: return - key = self.getPrivateKeyForPublicKey(account["options"]["memo_key"]) + key = self.getPrivateKeyForPublicKey( + account["options"]["memo_key"]) if key: return key return False @@ -288,8 +310,16 @@ def getAccountFromPrivateKey(self, wif): pub = format(PrivateKey(wif).pubkey, self.prefix) return self.getAccountFromPublicKey(pub) + def getAccountsFromPublicKey(self, pub): + """ Obtain all accounts associated with a public key + """ + names = self.rpc.get_key_references([pub]) + for name in names: + for i in name: + yield i + def getAccountFromPublicKey(self, pub): - """ Obtain account name from public key + """ Obtain the first account name from public key """ # FIXME, this only returns the first associated key. # If the key is used by multiple accounts, this @@ -300,26 +330,36 @@ def getAccountFromPublicKey(self, pub): else: return names[0] + def getAllAccounts(self, pub): + """ Get the account data for a public key (all accounts found for this + public key) + """ + for id in self.getAccountsFromPublicKey(pub): + try: + account = Account(id) # FIXME: self.bitshares is not available in wallet! + except: + continue + yield {"name": account["name"], + "account": account, + "type": self.getKeyType(account, pub), + "pubkey": pub} + def getAccount(self, pub): - """ Get the account data for a public key + """ Get the account data for a public key (first account found for this + public key) """ name = self.getAccountFromPublicKey(pub) if not name: - return {"name": None, - "type": None, - "pubkey": pub - } + return {"name": None, "type": None, "pubkey": pub} else: try: - account = Account(name) + account = Account(name) # FIXME: self.bitshares is not available in wallet! except: return - keyType = self.getKeyType(account, pub) return {"name": account["name"], "account": account, - "type": keyType, - "pubkey": pub - } + "type": self.getKeyType(account, pub), + "pubkey": pub} def getKeyType(self, account, pub): """ Get key type @@ -340,7 +380,7 @@ def getAccounts(self): for pubkey in pubkeys: # Filter those keys not for our network if pubkey[:len(self.prefix)] == self.prefix: - accounts.append(self.getAccount(pubkey)) + accounts.extend(self.getAllAccounts(pubkey)) return accounts def getPublicKeys(self): diff --git a/bitshares/witness.py b/bitshares/witness.py index b127e312..bd437935 100644 --- a/bitshares/witness.py +++ b/bitshares/witness.py @@ -8,44 +8,43 @@ class Witness(BlockchainObject): """ Read data about a witness in the chain :param str account_name: Name of the witness - :param bitshares bitshares_instance: BitShares() instance to use when accesing a RPC + :param bitshares bitshares_instance: BitShares() instance to use when + accesing a RPC """ type_ids = [6, 2] def refresh(self): - parts = self.identifier.split(".") - valid_objectid = False - try: - [int(x) for x in parts] - valid_objectid = True - except: - pass - if valid_objectid and len(parts) > 2: - if int(parts[1]) == 6: + if self.test_valid_objectid(self.identifier): + _, i, _ = self.identifier.split(".") + if int(i) == 6: witness = self.bitshares.rpc.get_object(self.identifier) else: - witness = self.bitshares.rpc.get_witness_by_account(self.identifier) + witness = self.bitshares.rpc.get_witness_by_account( + self.identifier) else: - account = Account(self.identifier, bitshares_instance=self.bitshares) + account = Account( + self.identifier, bitshares_instance=self.bitshares) witness = self.bitshares.rpc.get_witness_by_account(account["id"]) if not witness: raise WitnessDoesNotExistsException - super(Witness, self).__init__(witness) + super(Witness, self).__init__(witness, bitshares_instance=self.bitshares) @property def account(self): - return Account(self["witness_account"]) + return Account(self["witness_account"], bitshares_instance=self.bitshares) class Witnesses(list): """ Obtain a list of **active** witnesses and the current schedule - :param bitshares bitshares_instance: BitShares() instance to use when accesing a RPC + :param bitshares bitshares_instance: BitShares() instance to use when + accesing a RPC """ def __init__(self, bitshares_instance=None): self.bitshares = bitshares_instance or shared_bitshares_instance() - self.schedule = self.bitshares.rpc.get_object("2.12.0").get("current_shuffled_witnesses", []) + self.schedule = self.bitshares.rpc.get_object( + "2.12.0").get("current_shuffled_witnesses", []) super(Witnesses, self).__init__( [ diff --git a/bitshares/worker.py b/bitshares/worker.py index bfdbbfa8..a1b2498d 100644 --- a/bitshares/worker.py +++ b/bitshares/worker.py @@ -9,7 +9,8 @@ class Worker(BlockchainObject): """ Read data about a worker in the chain :param str id: id of the worker - :param bitshares bitshares_instance: BitShares() instance to use when accesing a RPC + :param bitshares bitshares_instance: BitShares() instance to use when + accesing a RPC """ type_id = 14 @@ -20,24 +21,30 @@ def refresh(self): raise WorkerDoesNotExistsException worker["work_end_date"] = formatTimeString(worker["work_end_date"]) worker["work_begin_date"] = formatTimeString(worker["work_begin_date"]) - super(Worker, self).__init__(worker) + super(Worker, self).__init__(worker, bitshares_instance=self.bitshares) self.cached = True @property def account(self): - return Account(self["worker_account"]) + return Account( + self["worker_account"], bitshares_instance=self.bitshares) class Workers(list): """ Obtain a list of workers for an account - :param str account_name/id: Name/id of the account - :param bitshares bitshares_instance: BitShares() instance to use when accesing a RPC + :param str account_name/id: Name/id of the account (optional) + :param bitshares bitshares_instance: BitShares() instance to use when + accesing a RPC """ - def __init__(self, account_name, bitshares_instance=None): + def __init__(self, account_name=None, bitshares_instance=None): self.bitshares = bitshares_instance or shared_bitshares_instance() - account = Account(account_name) - self.workers = self.bitshares.rpc.get_workers_by_account(account["id"]) + if account_name: + account = Account(account_name, bitshares_instance=self.bitshares) + self.workers = self.bitshares.rpc.get_workers_by_account( + account["id"]) + else: + self.workers = self.bitshares.rpc.get_all_workers() super(Workers, self).__init__( [ diff --git a/bitsharesbase/memo.py b/bitsharesbase/memo.py index c1045527..df8f584a 100644 --- a/bitsharesbase/memo.py +++ b/bitsharesbase/memo.py @@ -4,7 +4,7 @@ try: from Crypto.Cipher import AES except ImportError: - raise ImportError("Missing dependency: pycrypto") + raise ImportError("Missing dependency: pycryptodome") from .account import PrivateKey, PublicKey import struct @@ -114,5 +114,5 @@ def decode_memo(priv, pub, nonce, message): message = cleartext[4:] try: return _unpad(message.decode('utf8'), 16) - except: + except Exception as e: raise ValueError(message) diff --git a/bitsharesbase/objects.py b/bitsharesbase/objects.py index 38b8fe6a..2e96d9ae 100644 --- a/bitsharesbase/objects.py +++ b/bitsharesbase/objects.py @@ -9,7 +9,6 @@ ObjectId as GPHObjectId ) from graphenebase.objects import GrapheneObject, isArgsThisClass -from .chains import known_chains from .objecttypes import object_type from .account import PublicKey from graphenebase.objects import Operation as GPHOperation @@ -88,20 +87,8 @@ def __init__(self, *args, **kwargs): else: if len(args) == 1 and len(kwargs) == 0: kwargs = args[0] + prefix = kwargs.pop("prefix", default_prefix) if "message" in kwargs and kwargs["message"]: - if "chain" not in kwargs: - chain = default_prefix - else: - chain = kwargs["chain"] - if isinstance(chain, str) and chain in known_chains: - chain_params = known_chains[chain] - elif isinstance(chain, dict): - chain_params = chain - else: - raise Exception("Memo() only takes a string or a dict as chain!") - if "prefix" not in chain_params: - raise Exception("Memo() needs a 'prefix' in chain params!") - prefix = chain_params["prefix"] super().__init__(OrderedDict([ ('from', PublicKey(kwargs["from"], prefix=prefix)), ('to', PublicKey(kwargs["to"], prefix=prefix)), @@ -207,39 +194,44 @@ def __init__(self, *args, **kwargs): else: if len(args) == 1 and len(kwargs) == 0: kwargs = args[0] - - # Sorting - for key in [ - "whitelist_authorities", - "blacklist_authorities", - "whitelist_markets", - "blacklist_markets" - ]: - kwargs[key] = sorted( - set(kwargs[key]), - key=lambda x: int(x.split(".")[2]), - ) - super().__init__(OrderedDict([ - ('max_supply', Uint64(kwargs["max_supply"])), + ('max_supply', Int64(kwargs["max_supply"])), ('market_fee_percent', Uint16(kwargs["market_fee_percent"])), - ('max_market_fee', Uint64(kwargs["max_market_fee"])), + ('max_market_fee', Int64(kwargs["max_market_fee"])), ('issuer_permissions', Uint16(kwargs["issuer_permissions"])), ('flags', Uint16(kwargs["flags"])), ('core_exchange_rate', Price(kwargs["core_exchange_rate"])), ('whitelist_authorities', - Array([ObjectId(o, "account") for o in kwargs["whitelist_authorities"]])), + Array([ObjectId(x, "account") for x in kwargs["whitelist_authorities"]])), ('blacklist_authorities', - Array([ObjectId(o, "account") for o in kwargs["blacklist_authorities"]])), + Array([ObjectId(x, "account") for x in kwargs["blacklist_authorities"]])), ('whitelist_markets', - Array([ObjectId(o, "asset") for o in kwargs["whitelist_markets"]])), + Array([ObjectId(x, "asset") for x in kwargs["whitelist_markets"]])), ('blacklist_markets', - Array([ObjectId(o, "asset") for o in kwargs["blacklist_markets"]])), + Array([ObjectId(x, "asset") for x in kwargs["blacklist_markets"]])), ('description', String(kwargs["description"])), ('extensions', Set([])), ])) +class BitAssetOptions(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__(OrderedDict([ + ('feed_lifetime_sec', Uint32(kwargs["feed_lifetime_sec"])), + ('minimum_feeds', Uint8(kwargs["minimum_feeds"])), + ('force_settlement_delay_sec', Uint32(kwargs["force_settlement_delay_sec"])), + ('force_settlement_offset_percent', Uint16(kwargs["force_settlement_offset_percent"])), + ('maximum_force_settlement_volume', Uint16(kwargs["maximum_force_settlement_volume"])), + ('short_backing_asset', ObjectId(kwargs["short_backing_asset"], "asset")), + ('extensions', Set([])), + ])) + + class Worker_initializer(Static_variant): def __init__(self, o): diff --git a/bitsharesbase/operations.py b/bitsharesbase/operations.py index 157f74ca..696dd276 100644 --- a/bitsharesbase/operations.py +++ b/bitsharesbase/operations.py @@ -18,6 +18,7 @@ PriceFeed, Permission, AccountOptions, + BitAssetOptions, AssetOptions, ObjectId, Worker_initializer, @@ -39,13 +40,19 @@ def getOperationNameForId(i): class Transfer(GrapheneObject): def __init__(self, *args, **kwargs): + # Allow for overwrite of prefix if isArgsThisClass(self, args): self.data = args[0].data else: if len(args) == 1 and len(kwargs) == 0: kwargs = args[0] + prefix = kwargs.get("prefix", default_prefix) if "memo" in kwargs and kwargs["memo"]: - memo = Optional(Memo(kwargs["memo"])) + if isinstance(kwargs["memo"], dict): + kwargs["memo"]["prefix"] = prefix + memo = Optional(Memo(**kwargs["memo"])) + else: + memo = Optional(Memo(kwargs["memo"])) else: memo = Optional(None) super().__init__(OrderedDict([ @@ -74,14 +81,36 @@ def __init__(self, *args, **kwargs): ])) -class Asset_update(GrapheneObject): +class Asset_create(GrapheneObject): def __init__(self, *args, **kwargs): if isArgsThisClass(self, args): - self.data = args[0].data + self.data = args[0].data else: if len(args) == 1 and len(kwargs) == 0: kwargs = args[0] + if "bitasset_opts" in kwargs: + bitasset_opts = Optional(BitAssetOptions(kwargs["bitasset_opts"])) + else: + bitasset_opts = Optional(None) + super().__init__(OrderedDict([ + ('fee', Asset(kwargs["fee"])), + ('issuer', ObjectId(kwargs["issuer"], "account")), + ('symbol', String(kwargs["symbol"])), + ('precision', Uint8(kwargs["precision"])), + ('common_options', AssetOptions(kwargs["common_options"])), + ('bitasset_opts', bitasset_opts), + ('is_prediction_market', Bool(bool(kwargs['is_prediction_market']))), + ('extensions', Set([])), + ])) + +class Asset_update(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] if "new_issuer" in kwargs: new_issuer = Optional(ObjectId(kwargs["new_issuer"], "account")) else: @@ -96,6 +125,45 @@ def __init__(self, *args, **kwargs): ])) +class Asset_update_bitasset(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + super().__init__(OrderedDict([ + ('fee', Asset(kwargs["fee"])), + ('issuer', ObjectId(kwargs["issuer"], "account")), + ('asset_to_update', ObjectId(kwargs["asset_to_update"], "asset")), + ('new_options', BitAssetOptions(kwargs["new_options"])), + ('extensions', Set([])), + ])) + + +class Asset_issue(GrapheneObject): + def __init__(self, *args, **kwargs): + if isArgsThisClass(self, args): + self.data = args[0].data + else: + prefix = kwargs.get("prefix", default_prefix) + + if len(args) == 1 and len(kwargs) == 0: + kwargs = args[0] + if "memo" in kwargs and kwargs["memo"]: + memo = Optional(Memo(prefix=prefix, **kwargs["memo"])) + else: + memo = Optional(None) + super().__init__(OrderedDict([ + ('fee', Asset(kwargs["fee"])), + ('issuer', ObjectId(kwargs["issuer"], "account")), + ('asset_to_issue', Asset(kwargs["asset_to_issue"])), + ('issue_to_account', ObjectId(kwargs["issue_to_account"], "account")), + ('memo', memo), + ('extensions', Set([])), + ])) + + class Op_wrapper(GrapheneObject): def __init__(self, *args, **kwargs): if isArgsThisClass(self, args): diff --git a/docs/index.rst b/docs/index.rst index 98dd5256..a25518c8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -126,6 +126,7 @@ Python-BitShares Libraries price vesting witness + worker proposal Low Level Classes diff --git a/docs/memo.rst b/docs/memo.rst index 1f1283a1..c4992887 100644 --- a/docs/memo.rst +++ b/docs/memo.rst @@ -50,8 +50,8 @@ of the message. Example ####### -High Level -~~~~~~~~~~ +Encrypting a memo +~~~~~~~~~~~~~~~~~ The high level memo class makes use of the pybitshares wallet to obtain keys for the corresponding accounts. @@ -65,34 +65,36 @@ for the corresponding accounts. from_account=Account(from_account), to_account=Account(to_account) ) - cipher = memoObj.encrypt(memo) - plain = memoObj.decrypt(cipher) + encrypted_memo = memoObj.encrypt(memo) - -Low Level -~~~~~~~~~ +Decoding of a received memo +~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python - from bitsharesbase.memo import memo - from bitsharesbase.account import PrivateKey, PublicKey + from getpass import getpass + from bitshares.block import Block + from bitshares.memo import Memo - wifkey = "5...." - memo = { - "from": "GPH5mgup8evDqMnT86L7scVebRYDC2fwAWmygPEUL43LjstQegYCC", - "to": "GPH5Ar4j53kFWuEZQ9XhxbAja4YXMPJ2EnUg5QcrdeMFYUNMMNJbe", - "nonce": "13043867485137706821", - "message": "d55524c37320920844ca83bb20c8d008" - } - try : - privkey = PrivateKey(wifkey) - pubkey = PublicKey(memo["from"], prefix=prefix) - memomsg = memo.decode_memo(privkey, pubkey, memo["nonce"], memo["message"]) - except Exception as e: - memomsg = "--cannot decode-- %s" % str(e) - -Definitions -########### + # Obtain a transfer from the blockchain + block = Block(23755086) # block + transaction = block["transactions"][3] # transactions + op = transaction["operations"][0] # operation + op_id = op[0] # operation type + op_data = op[1] # operation payload + + # Instantiate Memo for decoding + memo = Memo() + + # Unlock wallet + memo.unlock_wallet(getpass()) + + # Decode memo + # Raises exception if required keys not available in the wallet + print(memo.decrypt(op_data["memo"])) + +API +### .. automodule:: bitsharesbase.memo :members: diff --git a/docs/requirements.txt b/docs/requirements.txt index 5f01cdad..81d5c8bd 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,5 +1,5 @@ graphenelib bitshares autobahn>=0.14 -pycrypto==2.6.1 -appdirs==1.4.0 +pycryptodome==3.4.6 +appdirs==1.4.0 \ No newline at end of file diff --git a/requirements-test.txt b/requirements-test.txt index 0e485d2a..093b555d 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,7 +1,9 @@ graphenelib -pycrypto==2.6.1 +pycryptodome==3.4.6 scrypt==0.7.1 Events==0.2.2 pyyaml pytest +pytest-mock coverage +mock diff --git a/setup.py b/setup.py index f1ee0049..a6dc898b 100755 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ ascii = codecs.lookup('ascii') codecs.register(lambda name, enc=ascii: {True: enc}.get(name == 'mbcs')) -VERSION = '0.1.8' +VERSION = '0.1.10' setup( name='bitshares', @@ -40,12 +40,12 @@ 'Topic :: Office/Business :: Financial', ], install_requires=[ - "graphenelib>=0.5.3", + "graphenelib>=0.5.8", "websockets", "appdirs", "Events", "scrypt", - "pycrypto", # for AES, installed through graphenelib already + "pycryptodome", # for AES, installed through graphenelib already ], setup_requires=['pytest-runner'], tests_require=['pytest'], diff --git a/tests/.ropeproject/globalnames b/tests/.ropeproject/globalnames new file mode 100644 index 00000000..08fd0f76 --- /dev/null +++ b/tests/.ropeproject/globalnames @@ -0,0 +1,4 @@ +€}q(Ubitsharesbase.operations]q(Udefault_prefixqUAsset_fund_fee_poolqUTransferqUAsset_publish_feedqUBid_collateralqUgetOperationNameForIdqUAccount_updateq U Asset_reserveq +U Worker_createq UAccount_upgradeq UAsset_update_feed_producersq UProposal_updateqUAccount_createqUVesting_balance_withdrawqUCall_order_updateqULimit_order_cancelqUAccount_whitelistqUWitness_updateqU +Op_wrapperqUOverride_transferqUProposal_createqU Asset_updateqULimit_order_createqeUpeerplays.committee]qU CommitteeqaU test_amount]qU TestcasesqaUtest_txbuffers]q(UwifqheUbitshares.transactionbuilder]q (Ulogq!UProposalBuilderq"UTransactionBuilderq#eUbitshares.bitshares]q$(h!U BitSharesq%eUbitshares.exceptions]q&(UNoWalletExceptionq'UBlockDoesNotExistsExceptionq(UAssetDoesNotExistsExceptionq)UInsufficientAuthorityErrorq*U%CommitteeMemberDoesNotExistsExceptionq+UMissingKeyErrorq,UWorkerDoesNotExistsExceptionq-UProposalDoesNotExistExceptionq.UInvalidAssetExceptionq/UWitnessDoesNotExistsExceptionq0U WalletExistsq1UWrongMasterPasswordExceptionq2UInvalidWifErrorq3U$VestingBalanceDoesNotExistsExceptionq4UObjectNotInProposalBufferq5UAccountDoesNotExistsExceptionq6UAccountExistsExceptionq7eUtest_proposals]q8(hheUbitshares.price]q9(U PriceFeedq:UUpdateCallOrderq;UOrderqeUbitsharesbase.objects]q?(hh>h:UAccountOptionsq@UWorker_initializerqAUMemoqBU +PermissionqCUAssetqDU AccountIdqEUAccountCreateExtensionsqFUAssetIdqGUSpecialAuthorityqHU ExtensionqIU OperationqJU AssetOptionsqKUObjectIdqLeUbitshares.account]qM(UAccountqNU AccountUpdateqOeUtest_objectcache]qPhaUtest_bitshares]qQ(hhU core_unitqReU test_wallet]qS(hheUpeerplays.witness]qT(U WitnessesqUUWitnessqVeu. \ No newline at end of file diff --git a/tests/.ropeproject/history b/tests/.ropeproject/history new file mode 100644 index 00000000..fcd9c963 --- /dev/null +++ b/tests/.ropeproject/history @@ -0,0 +1 @@ +€]q(]q]qe. \ No newline at end of file diff --git a/tests/.ropeproject/objectdb b/tests/.ropeproject/objectdb new file mode 100644 index 00000000..29c40cda --- /dev/null +++ b/tests/.ropeproject/objectdb @@ -0,0 +1 @@ +€}q. \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_account.py b/tests/test_account.py new file mode 100644 index 00000000..6dfe2aa8 --- /dev/null +++ b/tests/test_account.py @@ -0,0 +1,71 @@ +import unittest +import mock +from pprint import pprint +from bitshares import BitShares +from bitshares.account import Account +from bitshares.amount import Amount +from bitshares.asset import Asset +from bitshares.instance import set_shared_bitshares_instance +from bitsharesbase.operationids import getOperationNameForId + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + "wss://node.testnet.bitshares.eu", + nobroadcast=True, + # We want to bundle many operations into a single transaction + bundle=True, + # Overwrite wallet to use this list of wifs only + wif={"active": wif} + ) + self.bts.set_default_account("init0") + set_shared_bitshares_instance(self.bts) + + def test_account(self): + Account("witness-account") + Account("1.2.3") + asset = Asset("1.3.0") + symbol = asset["symbol"] + account = Account("witness-account", full=True) + self.assertEqual(account.name, "witness-account") + self.assertEqual(account["name"], account.name) + self.assertEqual(account["id"], "1.2.1") + self.assertIsInstance(account.balance("1.3.0"), Amount) + # self.assertIsInstance(account.balance({"symbol": symbol}), Amount) + self.assertIsInstance(account.balances, list) + for h in account.history(limit=1): + pass + + # BlockchainObjects method + account.cached = False + self.assertTrue(account.items()) + account.cached = False + self.assertIn("id", account) + account.cached = False + self.assertEqual(account["id"], "1.2.1") + self.assertEqual(str(account), "") + self.assertIsInstance(Account(account), Account) + + def test_account_upgrade(self): + account = Account("witness-account") + tx = account.upgrade() + ops = tx["operations"] + op = ops[0][1] + self.assertEqual(len(ops), 1) + self.assertEqual( + getOperationNameForId(ops[0][0]), + "account_upgrade" + ) + self.assertTrue( + op["upgrade_to_lifetime_member"] + ) + self.assertEqual( + op["account_to_upgrade"], + "1.2.1", + ) diff --git a/tests/test_aes.py b/tests/test_aes.py new file mode 100644 index 00000000..e2ad8e19 --- /dev/null +++ b/tests/test_aes.py @@ -0,0 +1,51 @@ +import string +import random +import unittest +import base64 +from pprint import pprint +from bitshares.aes import AESCipher + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.aes = AESCipher("Foobar") + + def test_str(self): + self.assertIsInstance(AESCipher.str_to_bytes("foobar"), bytes) + self.assertIsInstance(AESCipher.str_to_bytes(b"foobar"), bytes) + + def test_key(self): + self.assertEqual( + base64.b64encode(self.aes.key), + b"6BGBj4DZw8ItV3uoPWGWeI5VO7QIU1u0IQXN/3JqYKs=" + ) + + def test_pad(self): + self.assertEqual( + base64.b64encode(self.aes._pad(b"123456")), + b"MTIzNDU2GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGho=" + ) + + def test_unpad(self): + self.assertEqual( + self.aes._unpad(base64.b64decode(b"MTIzNDU2GhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGho=")), + b"123456" + ) + + def test_padding(self): + for n in range(1, 64): + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(n)) + self.assertEqual( + self.aes._unpad(self.aes._pad( + bytes(name, "utf-8"))), + bytes(name, "utf-8") + ) + + def test_encdec(self): + for n in range(1, 16): + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(64)) + self.assertEqual( + self.aes.decrypt(self.aes.encrypt(name)), + name) diff --git a/tests/test_amount.py b/tests/test_amount.py new file mode 100644 index 00000000..e255a95a --- /dev/null +++ b/tests/test_amount.py @@ -0,0 +1,225 @@ +import unittest +from bitshares import BitShares +from bitshares.amount import Amount +from bitshares.asset import Asset +from bitshares.instance import set_shared_bitshares_instance, SharedInstance + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + "wss://node.testnet.bitshares.eu", + nobroadcast=True, + ) + set_shared_bitshares_instance(self.bts) + self.asset = Asset("1.3.0") + self.symbol = self.asset["symbol"] + self.precision = self.asset["precision"] + self.asset2 = Asset("1.3.1") + + def dotest(self, ret, amount, symbol): + self.assertEqual(float(ret), float(amount)) + self.assertEqual(ret["symbol"], symbol) + self.assertIsInstance(ret["asset"], dict) + self.assertIsInstance(ret["amount"], float) + + def test_init(self): + # String init + amount = Amount("1 {}".format(self.symbol)) + self.dotest(amount, 1, self.symbol) + + # Amount init + amount = Amount(amount) + self.dotest(amount, 1, self.symbol) + + # blockchain dict init + amount = Amount({ + "amount": 1 * 10 ** self.precision, + "asset_id": self.asset["id"] + }) + self.dotest(amount, 1, self.symbol) + + # API dict init + amount = Amount({ + "amount": 1.3 * 10 ** self.precision, + "asset": self.asset["id"] + }) + self.dotest(amount, 1.3, self.symbol) + + # Asset as symbol + amount = Amount(1.3, Asset("1.3.0")) + self.dotest(amount, 1.3, self.symbol) + + # Asset as symbol + amount = Amount(1.3, self.symbol) + self.dotest(amount, 1.3, self.symbol) + + # keyword inits + amount = Amount(amount=1.3, asset=Asset("1.3.0")) + self.dotest(amount, 1.3, self.symbol) + + # keyword inits + amount = Amount(amount=1.3, asset=dict(Asset("1.3.0"))) + self.dotest(amount, 1.3, self.symbol) + + # keyword inits + amount = Amount(amount=1.3, asset=self.symbol) + self.dotest(amount, 1.3, self.symbol) + + def test_copy(self): + amount = Amount("1", self.symbol) + self.dotest(amount.copy(), 1, self.symbol) + + def test_properties(self): + amount = Amount("1", self.symbol) + self.assertEqual(amount.amount, 1.0) + self.assertEqual(amount.symbol, self.symbol) + self.assertIsInstance(amount.asset, Asset) + self.assertEqual(amount.asset["symbol"], self.symbol) + + def test_tuple(self): + amount = Amount("1", self.symbol) + self.assertEqual( + amount.tuple(), + (1.0, self.symbol)) + + def test_json(self): + amount = Amount("1", self.symbol) + self.assertEqual( + amount.json(), + { + "asset_id": self.asset["id"], + "amount": 1 * 10 ** self.precision + }) + + def test_string(self): + self.assertEqual( + str(Amount("1", self.symbol)), + "1.00000 {}".format(self.symbol)) + + def test_int(self): + self.assertEqual( + int(Amount("1", self.symbol)), + 100000) + + def test_float(self): + self.assertEqual( + float(Amount("1", self.symbol)), + 1.00000) + + def test_plus(self): + a1 = Amount(1, self.symbol) + a2 = Amount(2, self.symbol) + self.dotest(a1 + a2, 3, self.symbol) + with self.assertRaises(Exception): + a1 + Amount(1, asset=self.asset2) + # inline + a2 = Amount(2, self.symbol) + a2 += a1 + self.dotest(a2, 3, self.symbol) + a2 += 5 + self.dotest(a2, 8, self.symbol) + with self.assertRaises(Exception): + a1 += Amount(1, asset=self.asset2) + + def test_minus(self): + a1 = Amount(1, self.symbol) + a2 = Amount(2, self.symbol) + self.dotest(a1 - a2, -1, self.symbol) + self.dotest(a1 - 5, -4, self.symbol) + with self.assertRaises(Exception): + a1 - Amount(1, asset=self.asset2) + # inline + a2 = Amount(2, self.symbol) + a2 -= a1 + self.dotest(a2, 1, self.symbol) + a2 -= 1 + self.dotest(a2, 0, self.symbol) + self.dotest(a2 - 2, -2, self.symbol) + with self.assertRaises(Exception): + a1 -= Amount(1, asset=self.asset2) + + def test_mul(self): + a1 = Amount(5, self.symbol) + a2 = Amount(2, self.symbol) + self.dotest(a1 * a2, 10, self.symbol) + self.dotest(a1 * 3, 15, self.symbol) + with self.assertRaises(Exception): + a1 * Amount(1, asset=self.asset2) + # inline + a2 = Amount(2, self.symbol) + a2 *= 5 + self.dotest(a2, 10, self.symbol) + with self.assertRaises(Exception): + a1 *= Amount(2, asset=self.asset2) + + def test_div(self): + a1 = Amount(15, self.symbol) + self.dotest(a1 / 3, 5, self.symbol) + self.dotest(a1 // 2, 7, self.symbol) + with self.assertRaises(Exception): + a1 / Amount(1, asset=self.asset2) + # inline + a2 = a1.copy() + a2 /= 3 + self.dotest(a2, 5, self.symbol) + a2 = a1.copy() + a2 //= 2 + self.dotest(a2, 7, self.symbol) + with self.assertRaises(Exception): + a1 *= Amount(2, asset=self.asset2) + + def test_mod(self): + a1 = Amount(15, self.symbol) + self.dotest(a1 % 3, 0, self.symbol) + self.dotest(a1 % 2, 1, self.symbol) + with self.assertRaises(Exception): + a1 % Amount(1, asset=self.asset2) + # inline + a2 = a1.copy() + a2 %= 3 + self.dotest(a2, 0, self.symbol) + with self.assertRaises(Exception): + a1 %= Amount(2, asset=self.asset2) + + def test_pow(self): + a1 = Amount(15, self.symbol) + self.dotest(a1 ** 3, 15 ** 3, self.symbol) + self.dotest(a1 ** 2, 15 ** 2, self.symbol) + with self.assertRaises(Exception): + a1 ** Amount(1, asset=self.asset2) + # inline + a2 = a1.copy() + a2 **= 3 + self.dotest(a2, 15 ** 3, self.symbol) + with self.assertRaises(Exception): + a1 **= Amount(2, asset=self.asset2) + + def test_ltge(self): + a1 = Amount(1, self.symbol) + a2 = Amount(2, self.symbol) + self.assertTrue(a1 < a2) + self.assertTrue(a2 > a1) + self.assertTrue(a2 > 1) + self.assertTrue(a1 < 5) + + def test_leeq(self): + a1 = Amount(1, self.symbol) + a2 = Amount(1, self.symbol) + self.assertTrue(a1 <= a2) + self.assertTrue(a1 >= a2) + self.assertTrue(a1 <= 1) + self.assertTrue(a1 >= 1) + + def test_ne(self): + a1 = Amount(1, self.symbol) + a2 = Amount(2, self.symbol) + self.assertTrue(a1 != a2) + self.assertTrue(a1 != 5) + a1 = Amount(1, self.symbol) + a2 = Amount(1, self.symbol) + self.assertTrue(a1 == a2) + self.assertTrue(a1 == 1) diff --git a/tests/test_asset.py b/tests/test_asset.py new file mode 100644 index 00000000..c25df632 --- /dev/null +++ b/tests/test_asset.py @@ -0,0 +1,49 @@ +import unittest +from bitshares import BitShares +from bitshares.asset import Asset +from bitshares.instance import set_shared_bitshares_instance +from bitshares.exceptions import AssetDoesNotExistsException + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + nobroadcast=True, + ) + set_shared_bitshares_instance(self.bts) + + def test_assert(self): + with self.assertRaises(AssetDoesNotExistsException): + Asset("FOObarNonExisting", full=False) + + def test_refresh(self): + asset = Asset("1.3.0", full=False) + asset.ensure_full() + self.assertIn("dynamic_asset_data", asset) + self.assertIn("flags", asset) + self.assertIn("permissions", asset) + self.assertIsInstance(asset["flags"], dict) + self.assertIsInstance(asset["permissions"], dict) + + def test_properties(self): + asset = Asset("1.3.0", full=False) + self.assertIsInstance(asset.symbol, str) + self.assertIsInstance(asset.precision, int) + self.assertIsInstance(asset.is_bitasset, bool) + self.assertIsInstance(asset.permissions, dict) + self.assertEqual(asset.permissions, asset["permissions"]) + self.assertIsInstance(asset.flags, dict) + self.assertEqual(asset.flags, asset["flags"]) + + """ + # Mocker comes from pytest-mock, providing an easy way to have patched objects + # for the life of the test. + def test_calls(mocker): + asset = Asset("USD", lazy=True, bitshares_instance=BitShares(offline=True)) + method = mocker.patch.object(Asset, 'get_call_orders') + asset.calls + method.assert_called_with(10) + """ diff --git a/tests/test_base_objects.py b/tests/test_base_objects.py new file mode 100644 index 00000000..ca7c2900 --- /dev/null +++ b/tests/test_base_objects.py @@ -0,0 +1,31 @@ +import unittest +from bitshares import BitShares, exceptions +from bitshares.instance import set_shared_bitshares_instance +from bitshares.account import Account +from bitshares.committee import Committee + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + nobroadcast=True, + ) + set_shared_bitshares_instance(self.bts) + + def test_Committee(self): + with self.assertRaises( + exceptions.AccountDoesNotExistsException + ): + Committee("FOObarNonExisting") + + c = Committee("init0") + self.assertEqual(c["id"], "1.5.0") + self.assertIsInstance(c.account, Account) + + with self.assertRaises( + exceptions.CommitteeMemberDoesNotExistsException + ): + Committee("nathan") diff --git a/tests/test_bitshares.py b/tests/test_bitshares.py new file mode 100644 index 00000000..864de94d --- /dev/null +++ b/tests/test_bitshares.py @@ -0,0 +1,228 @@ +import mock +import string +import unittest +import random +from pprint import pprint +from bitshares import BitShares +from bitsharesbase.operationids import getOperationNameForId +from bitshares.amount import Amount +from bitsharesbase.account import PrivateKey +from bitshares.instance import set_shared_bitshares_instance + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +core_unit = "TEST" + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + "wss://node.testnet.bitshares.eu", + nobroadcast=True, + keys={"active": wif, "owner": wif, "memo": wif}, + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_bitshares_instance(self.bts) + self.bts.set_default_account("init0") + + def test_connect(self): + self.bts.connect() + + def test_set_default_account(self): + self.bts.set_default_account("init0") + + def test_info(self): + info = self.bts.info() + for key in ['current_witness', + 'head_block_id', + 'head_block_number', + 'id', + 'last_irreversible_block_num', + 'next_maintenance_time', + 'recently_missed_count', + 'time']: + self.assertTrue(key in info) + + def test_finalizeOps(self): + bts = self.bts + tx1 = bts.new_tx() + tx2 = bts.new_tx() + self.bts.transfer("init1", 1, core_unit, append_to=tx1) + self.bts.transfer("init1", 2, core_unit, append_to=tx2) + self.bts.transfer("init1", 3, core_unit, append_to=tx1) + tx1 = tx1.json() + tx2 = tx2.json() + ops1 = tx1["operations"] + ops2 = tx2["operations"] + self.assertEqual(len(ops1), 2) + self.assertEqual(len(ops2), 1) + + def test_transfer(self): + bts = self.bts + tx = bts.transfer( + "1.2.8", 1.33, core_unit, memo="Foobar", account="1.2.7") + self.assertEqual( + getOperationNameForId(tx["operations"][0][0]), + "transfer" + ) + op = tx["operations"][0][1] + self.assertIn("memo", op) + self.assertEqual(op["from"], "1.2.7") + self.assertEqual(op["to"], "1.2.8") + amount = Amount(op["amount"]) + self.assertEqual(float(amount), 1.33) + + def test_create_account(self): + bts = self.bts + name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) + key1 = PrivateKey() + key2 = PrivateKey() + key3 = PrivateKey() + key4 = PrivateKey() + tx = bts.create_account( + name, + registrar="init0", # 1.2.7 + referrer="init1", # 1.2.8 + referrer_percent=33, + owner_key=format(key1.pubkey, core_unit), + active_key=format(key2.pubkey, core_unit), + memo_key=format(key3.pubkey, core_unit), + additional_owner_keys=[format(key4.pubkey, core_unit)], + additional_active_keys=[format(key4.pubkey, core_unit)], + additional_owner_accounts=["committee-account"], # 1.2.0 + additional_active_accounts=["committee-account"], + proxy_account="init0", + storekeys=False + ) + self.assertEqual( + getOperationNameForId(tx["operations"][0][0]), + "account_create" + ) + op = tx["operations"][0][1] + role = "active" + self.assertIn( + format(key4.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key4.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "1.2.0", + [x[0] for x in op[role]["account_auths"]]) + role = "owner" + self.assertIn( + format(key4.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + format(key4.pubkey, core_unit), + [x[0] for x in op[role]["key_auths"]]) + self.assertIn( + "1.2.0", + [x[0] for x in op[role]["account_auths"]]) + self.assertEqual( + op["options"]["voting_account"], + "1.2.6") + self.assertEqual( + op["registrar"], + "1.2.6") + self.assertEqual( + op["referrer"], + "1.2.7") + self.assertEqual( + op["referrer_percent"], + 33 * 100) + + def test_weight_threshold(self): + bts = self.bts + + auth = {'account_auths': [['1.2.0', '1']], + 'extensions': [], + 'key_auths': [ + ['TEST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n', 1], + ['TEST7GM9YXcsoAJAgKbqW2oVj7bnNXFNL4pk9NugqKWPmuhoEDbkDv', 1]], + 'weight_threshold': 3} # threshold fine + bts._test_weights_treshold(auth) + auth = {'account_auths': [['1.2.0', '1']], + 'extensions': [], + 'key_auths': [ + ['TEST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n', 1], + ['TEST7GM9YXcsoAJAgKbqW2oVj7bnNXFNL4pk9NugqKWPmuhoEDbkDv', 1]], + 'weight_threshold': 4} # too high + + with self.assertRaises(ValueError): + bts._test_weights_treshold(auth) + + def test_allow(self): + bts = self.bts + tx = bts.allow( + "TEST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n", + weight=1, + threshold=1, + permission="owner" + ) + self.assertEqual( + getOperationNameForId(tx["operations"][0][0]), + "account_update" + ) + op = tx["operations"][0][1] + self.assertIn("owner", op) + self.assertIn( + ["TEST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n", '1'], + op["owner"]["key_auths"]) + self.assertEqual(op["owner"]["weight_threshold"], 1) + + def test_disallow(self): + bts = self.bts + with self.assertRaisesRegex(ValueError, ".*Changes nothing.*"): + bts.disallow( + "TEST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n", + weight=1, + threshold=1, + permission="owner" + ) + with self.assertRaisesRegex(ValueError, ".*Changes nothing!.*"): + bts.disallow( + "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + weight=1, + threshold=1, + permission="owner" + ) + + def test_update_memo_key(self): + bts = self.bts + tx = bts.update_memo_key("TEST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n") + self.assertEqual( + getOperationNameForId(tx["operations"][0][0]), + "account_update" + ) + op = tx["operations"][0][1] + self.assertEqual( + op["new_options"]["memo_key"], + "TEST55VCzsb47NZwWe5F3qyQKedX9iHBHMVVFSc96PDvV7wuj7W86n") + + def test_approvewitness(self): + bts = self.bts + tx = bts.approvewitness("init0") + self.assertEqual( + getOperationNameForId(tx["operations"][0][0]), + "account_update" + ) + op = tx["operations"][0][1] + self.assertIn( + "1:0", + op["new_options"]["votes"]) + + def test_approvecommittee(self): + bts = self.bts + tx = bts.approvecommittee("init0") + self.assertEqual( + getOperationNameForId(tx["operations"][0][0]), + "account_update" + ) + op = tx["operations"][0][1] + self.assertIn( + "0:11", + op["new_options"]["votes"]) diff --git a/tests/test_message.py b/tests/test_message.py new file mode 100644 index 00000000..76441160 --- /dev/null +++ b/tests/test_message.py @@ -0,0 +1,75 @@ +import unittest +import mock +from bitshares import BitShares +from bitshares.message import Message +from bitshares.instance import set_shared_bitshares_instance + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +core_unit = "PPY" + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + nobroadcast=True, + wif=[wif] + ) + set_shared_bitshares_instance(self.bts) + + def test_sign_message(self): + def new_refresh(self): + dict.__init__( + self, { + "name": "init0", + "options": { + "memo_key": "BTS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + }}) + + with mock.patch( + "bitshares.account.Account.refresh", + new=new_refresh + ): + p = Message("message foobar").sign() + Message(p).verify() + + def test_verify_message(self): + def new_refresh(self): + dict.__init__( + self, { + "name": "init0", + "options": { + "memo_key": "BTS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + }}) + + with mock.patch( + "bitshares.account.Account.refresh", + new=new_refresh + ): + Message( + "-----BEGIN BITSHARES SIGNED MESSAGE-----\n" + "message foobar\n" + "-----BEGIN META-----\n" + "account=init0\n" + "memokey=BTS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV\n" + "block=23814223\n" + "timestamp=2018-01-24T11:42:33\n" + "-----BEGIN SIGNATURE-----\n" + "2034f601e175a25cf9f60a828650301f57c9efab53929b6a82fb413feb8a786fcb3ba4238dd8bece03aee38526ee363324d43944d4a3f9dc624fbe53ef5f0c9a5e\n" + "-----END BITSHARES SIGNED MESSAGE-----\n" + ).verify() + + Message( + "-----BEGIN BITSHARES SIGNED MESSAGE-----" + "message foobar\n" + "-----BEGIN META-----" + "account=init0\n" + "memokey=BTS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV\n" + "block=23814223\n" + "timestamp=2018-01-24T11:42:33" + "-----BEGIN SIGNATURE-----" + "2034f601e175a25cf9f60a828650301f57c9efab53929b6a82fb413feb8a786fcb3ba4238dd8bece03aee38526ee363324d43944d4a3f9dc624fbe53ef5f0c9a5e\n" + "-----END BITSHARES SIGNED MESSAGE-----" + ).verify() diff --git a/tests/test_objectcache.py b/tests/test_objectcache.py new file mode 100644 index 00000000..aa1a03fc --- /dev/null +++ b/tests/test_objectcache.py @@ -0,0 +1,33 @@ +import time +import unittest +from bitshares import BitShares, exceptions +from bitshares.instance import set_shared_bitshares_instance +from bitshares.blockchainobject import ObjectCache + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + nobroadcast=True, + ) + set_shared_bitshares_instance(self.bts) + + def test_cache(self): + cache = ObjectCache(default_expiration=1) + self.assertEqual(str(cache), "ObjectCache(n=0, default_expiration=1)") + + # Data + cache["foo"] = "bar" + self.assertIn("foo", cache) + self.assertEqual(cache["foo"], "bar") + self.assertEqual(cache.get("foo", "New"), "bar") + + # Expiration + time.sleep(2) + self.assertNotIn("foo", cache) + + # Get + self.assertEqual(cache.get("foo", "New"), "New") diff --git a/tests/test_price.py b/tests/test_price.py index 9c12e114..808577b0 100644 --- a/tests/test_price.py +++ b/tests/test_price.py @@ -11,7 +11,8 @@ class Testcases(unittest.TestCase): def __init__(self, *args, **kwargs): super(Testcases, self).__init__(*args, **kwargs) bitshares = BitShares( - "wss://node.bitshares.eu" + "wss://node.bitshares.eu", + nobroadcast=True, ) set_shared_bitshares_instance(bitshares) @@ -35,14 +36,14 @@ def test_init(self): def test_multiplication(self): p1 = Price(10.0, "USD/GOLD") - p2 = Price(5.0, "USD/EUR") + p2 = Price(5.0, "EUR/USD") p3 = p1 * p2 p4 = p3.as_base("GOLD") self.assertEqual(p4["quote"]["symbol"], "EUR") self.assertEqual(p4["base"]["symbol"], "GOLD") - # 10 USD/GOLD * 0.2 EUR/USD = 2 EUR/GOLD = 0.5 GOLD/EUR - self.assertEqual(float(p4), 0.5) + # 10 USD/GOLD * 0.2 EUR/USD = 50 EUR/GOLD = 0.02 GOLD/EUR + self.assertEqual(float(p4), 0.02) # Inline multiplication p5 = p1 @@ -50,8 +51,8 @@ def test_multiplication(self): p4 = p5.as_base("GOLD") self.assertEqual(p4["quote"]["symbol"], "EUR") self.assertEqual(p4["base"]["symbol"], "GOLD") - # 10 USD/GOLD * 0.2 EUR/USD = 2 EUR/GOLD = 0.5 GOLD/EUR - self.assertEqual(float(p4), 0.5) + # 10 USD/GOLD * 0.2 EUR/USD = 2 EUR/GOLD = 0.02 GOLD/EUR + self.assertEqual(float(p4), 0.02) def test_div(self): p1 = Price(10.0, "USD/GOLD") diff --git a/tests/test_proposals.py b/tests/test_proposals.py new file mode 100644 index 00000000..d92b23ed --- /dev/null +++ b/tests/test_proposals.py @@ -0,0 +1,123 @@ +import unittest +from pprint import pprint +from bitshares import BitShares +from bitsharesbase.operationids import getOperationNameForId +from bitshares.instance import set_shared_bitshares_instance + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + "wss://node.testnet.bitshares.eu", + nobroadcast=True, + keys={"active": wif}, + ) + # from getpass import getpass + # self.bts.wallet.unlock(getpass()) + set_shared_bitshares_instance(self.bts) + self.bts.set_default_account("init0") + + def test_finalizeOps_proposal(self): + bts = self.bts + # proposal = bts.new_proposal(bts.tx()) + proposal = bts.proposal() + self.bts.transfer("init1", 1, "TEST", append_to=proposal) + tx = bts.tx().json() # default tx buffer + ops = tx["operations"] + self.assertEqual(len(ops), 1) + self.assertEqual( + getOperationNameForId(ops[0][0]), + "proposal_create") + prop = ops[0][1] + self.assertEqual(len(prop["proposed_ops"]), 1) + self.assertEqual( + getOperationNameForId(prop["proposed_ops"][0]["op"][0]), + "transfer") + + def test_finalizeOps_proposal2(self): + bts = self.bts + proposal = bts.new_proposal() + # proposal = bts.proposal() + self.bts.transfer("init1", 1, "TEST", append_to=proposal) + tx = bts.tx().json() # default tx buffer + ops = tx["operations"] + self.assertEqual(len(ops), 1) + self.assertEqual( + getOperationNameForId(ops[0][0]), + "proposal_create") + prop = ops[0][1] + self.assertEqual(len(prop["proposed_ops"]), 1) + self.assertEqual( + getOperationNameForId(prop["proposed_ops"][0]["op"][0]), + "transfer") + + def test_finalizeOps_combined_proposal(self): + bts = self.bts + parent = bts.new_tx() + proposal = bts.new_proposal(parent) + self.bts.transfer("init1", 1, "TEST", append_to=proposal) + self.bts.transfer("init1", 1, "TEST", append_to=parent) + tx = parent.json() + ops = tx["operations"] + self.assertEqual(len(ops), 2) + self.assertEqual( + getOperationNameForId(ops[0][0]), + "proposal_create") + self.assertEqual( + getOperationNameForId(ops[1][0]), + "transfer") + prop = ops[0][1] + self.assertEqual(len(prop["proposed_ops"]), 1) + self.assertEqual( + getOperationNameForId(prop["proposed_ops"][0]["op"][0]), + "transfer") + + def test_finalizeOps_changeproposer_new(self): + bts = self.bts + proposal = bts.proposal(proposer="init5") + bts.transfer("init1", 1, "TEST", append_to=proposal) + tx = bts.tx().json() + ops = tx["operations"] + self.assertEqual(len(ops), 1) + self.assertEqual( + getOperationNameForId(ops[0][0]), + "proposal_create") + prop = ops[0][1] + self.assertEqual(len(prop["proposed_ops"]), 1) + self.assertEqual(prop["fee_paying_account"], "1.2.11") + self.assertEqual( + getOperationNameForId(prop["proposed_ops"][0]["op"][0]), + "transfer") + + def test_finalizeOps_changeproposer_legacy(self): + bts = self.bts + bts.proposer = "init5" + tx = bts.transfer("init1", 1, "TEST") + ops = tx["operations"] + self.assertEqual(len(ops), 1) + self.assertEqual( + getOperationNameForId(ops[0][0]), + "proposal_create") + prop = ops[0][1] + self.assertEqual(len(prop["proposed_ops"]), 1) + self.assertEqual(prop["fee_paying_account"], "1.2.11") + self.assertEqual( + getOperationNameForId(prop["proposed_ops"][0]["op"][0]), + "transfer") + + def test_new_proposals(self): + bts = self.bts + p1 = bts.new_proposal() + p2 = bts.new_proposal() + self.assertIsNotNone(id(p1), id(p2)) + + def test_new_txs(self): + bts = self.bts + p1 = bts.new_tx() + p2 = bts.new_tx() + self.assertIsNotNone(id(p1), id(p2)) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 9d015cb5..14a61ee5 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -117,7 +117,7 @@ def test_proposal_update(self): "4fb5093226275f48a42d9e8cf") self.doit() - def test_Transfer(self): + def test_transfer(self): pub = format(account.PrivateKey(wif).pubkey, prefix) from_account_id = "1.2.0" to_account_id = "1.2.1" @@ -139,7 +139,6 @@ def test_Transfer(self): "to": pub, "nonce": nonce, "message": encrypted_memo, - "chain": prefix } memoObj = objects.Memo(**memoStruct) self.op = operations.Transfer(**{ @@ -147,7 +146,8 @@ def test_Transfer(self): "from": from_account_id, "to": to_account_id, "amount": amount, - "memo": memoObj + "memo": memoObj, + "prefix": prefix }) self.cm = ("f68585abf4dce7c804570100000000000000000000000140420" "f0000000000040102c0ded2bc1f1305fb0faac5e6c03ee3a192" @@ -264,7 +264,8 @@ def test_create_account(self): "owner_special_authority": [1, {"asset": "1.3.127", "num_top_holders": 10}] - } + }, + "prefix": "BTS" }) self.cm = ("f68585abf4dce7c804570105f26416000000000000211b03000b666f" "6f6261722d6631323401000000000202fe8cc11cc8251de6977636b5" @@ -306,7 +307,8 @@ def test_update_account(self): "votes": [], "extensions": [] }, - "extensions": {} + "extensions": {}, + "prefix": "BTS" }) self.cm = ("f68585abf4dce7c804570106f264160000000000000" "f010100000001d6ee0501000102fe8cc11cc8251de6" @@ -348,42 +350,6 @@ def test_create_proposal(self): "a64769f5f62c0301ce21ab4f7c67a6801b4266") self.doit() - def test_asset_update(self): - self.op = operations.Asset_update(**{ - "fee": {"amount": 0, - "asset_id": "1.3.0"}, - "issuer": "1.2.0", - "asset_to_update": "1.3.0", - "new_options": { - "max_supply": "1000000000000000", - "market_fee_percent": 0, - "max_market_fee": "1000000000000000", - "issuer_permissions": 79, - "flags": 0, - "core_exchange_rate": { - "base": {"amount": 0, - "asset_id": "1.3.0"}, - "quote": {"amount": 0, - "asset_id": "1.3.0"} - }, - "whitelist_authorities": ["1.2.12", "1.2.13"], - "blacklist_authorities": ["1.2.10", "1.2.11"], - "whitelist_markets": ["1.3.10", "1.3.11"], - "blacklist_markets": ["1.3.12", "1.3.13"], - "description": "Foobar", - "extensions": [] - }, - "extensions": [] - }) - self.cm = ("f68585abf4dce7c80457010b00000000000000000000000000" - "80c6a47e8d030000000080c6a47e8d03004f00000000000000" - "0000000000000000000000000000020c0d020a0b020a0b020c" - "0d06466f6f626172000000011f5bd6a206d210d1d78eb423e0" - "c2362013aa80830a8e61e5df2570eac05f1c57a4165c99099f" - "c2e97ecbf2b46014c96a6f99cff8d20f55a6042929136055e5" - "ad10") - self.doit() - def test_whitelist(self): self.op = operations.Account_whitelist(**{ "fee": {"amount": 0, @@ -471,19 +437,6 @@ def test_asset_reserve(self): "40c241db9cad86e27369d0e5a76b5832d585505ff177d") self.doit() - def test_asset_reserve(self): - self.op = operations.Asset_reserve(**{ - "fee": {"amount": 0, "asset_id": "1.3.0"}, - "payer": "1.2.0", - "amount_to_reserve": {"amount": 1234567890, "asset_id": "1.3.0"}, - "extensions": [] - }) - self.cm = ("f68585abf4dce7c80457010f00000000000000000000d202964" - "900000000000000011f75065cb1155bfcaabaf55d3357d69679" - "c7c1fe589b6dc0919fe1dde1a305009c360823a40c28907299a" - "40c241db9cad86e27369d0e5a76b5832d585505ff177d") - self.doit() - def test_bid_collateral(self): self.op = operations.Bid_collateral(**{ 'fee': {'amount': 100, @@ -504,6 +457,167 @@ def test_bid_collateral(self): "2e94750ce2c5") self.doit() + def test_asset_create(self): + self.op = operations.Asset_create(**{ + "fee": { + "amount": 0, + "asset_id": "1.3.0" + }, + "issuer": "1.2.0", + "symbol": "THING", + "precision": 0, + "common_options": { + "max_supply": "1000000000000000", + "market_fee_percent": 0, + "max_market_fee": "1000000000000000", + "issuer_permissions": 79, + "flags": 0, + "core_exchange_rate": { + "base": { + "amount": 0, + "asset_id": "1.3.0" + }, + "quote": { + "amount": 0, + "asset_id": "1.3.0" + } + }, + "whitelist_authorities": ["1.2.0"], + "blacklist_authorities": ["1.2.1"], + "whitelist_markets": ["1.3.0"], + "blacklist_markets": ["1.3.1"], + "description": "Foobar think", + "extensions": [] + }, + "bitasset_opts": { + "feed_lifetime_sec": 86400, + "minimum_feeds": 7, + "force_settlement_delay_sec": 86400, + "force_settlement_offset_percent": 100, + "maximum_force_settlement_volume": 50, + "short_backing_asset": "1.3.0", + "extensions": [] + }, + "is_prediction_market": False, + "extensions": [] + }) + self.cm = ("f68585abf4dce7c80457010a000000000000000000000554484" + "94e47000080c6a47e8d030000000080c6a47e8d03004f000000" + "000000000000000000000000000000000000010001010100010" + "10c466f6f626172207468696e6b000180510100078051010064" + "0032000000000000011f1b8ac491bb327921d9346d543e530d8" + "8acb68bade58296a7a27b0a74a28eaca762260dbb905a6415f6" + "225a8028a810de6290badc29d16fea0ffd88bc8c0cbec4") + self.doit() + + def test_asset_update(self): + self.op = operations.Asset_update(**{ + "fee": { + "amount": 0, + "asset_id": "1.3.0" + }, + "issuer": "1.2.0", + "asset_to_update": "1.3.0", + "new_options": { + "max_supply": "1000000000000000", + "market_fee_percent": 0, + "max_market_fee": "1000000000000000", + "issuer_permissions": 79, + "flags": 0, + "core_exchange_rate": { + "base": { + "amount": 0, + "asset_id": "1.3.0" + }, + "quote": { + "amount": 0, + "asset_id": "1.3.0" + } + }, + "whitelist_authorities": [], + "blacklist_authorities": [], + "whitelist_markets": [], + "blacklist_markets": [], + "description": "", + "extensions": [] + }, + "extensions": [] + }) + self.cm = ("f68585abf4dce7c80457010b000000000000000000000000008" + "0c6a47e8d030000000080c6a47e8d03004f0000000000000000" + "000000000000000000000000000000000000000000011f51477" + "1af6ac47a12a387979b6452afcd3f50514277efd7938f5227a7" + "fe7287db529d251e2b7c31d4a2d8ed59035b78b64f95e6011d9" + "58ab9504008a56c83cbb6") + self.doit() + + def test_asset_update_bitasset(self): + self.op = operations.Asset_update_bitasset(**{ + "fee": { + "amount": 0, + "asset_id": "1.3.0" + }, + "issuer": "1.2.0", + "asset_to_update": "1.3.0", + "new_options": { + "feed_lifetime_sec": 86400, + "minimum_feeds": 1, + "force_settlement_delay_sec": 86400, + "force_settlement_offset_percent": 0, + "maximum_force_settlement_volume": 2000, + "short_backing_asset": "1.3.0", + "extensions": [] + }, + "extensions": [] + }) + self.cm = ("f68585abf4dce7c80457010c000000000000000000000080510" + "10001805101000000d0070000000001205e7fed2110783b4fe9" + "ec1f1a71ad0325fce04fd11d03a534baac5cf18c52c91e6fdae" + "b76cff9d480a96500cbfde214cadd436e8f66aa61ad3f14973e" + "42406eca") + self.doit() + + def test_asset_issue(self): + message = "abcdefgABCDEFG0123456789" + nonce = "5862723643998573708" + pub = format(account.PrivateKey(wif).pubkey, prefix) + encrypted_memo = memo.encode_memo( + account.PrivateKey(wif), + account.PublicKey(pub, prefix=prefix), + nonce, + message + ) + self.op = operations.Asset_issue(**{ + "fee": { + "amount": 0, + "asset_id": "1.3.0" + }, + "issuer": "1.2.0", + "asset_to_issue": { + "amount": 0, + "asset_id": "1.3.0" + }, + "memo": { + "from": pub, + "to": pub, + "nonce": nonce, + "message": encrypted_memo, + }, + "issue_to_account": "1.2.0", + "extensions": [], + "prefix": prefix + }) + self.cm = ("f68585abf4dce7c80457010e000000000000000000000000000" + "00000000000000102c0ded2bc1f1305fb0faac5e6c03ee3a192" + "4234985427b6167ca569d13df435cf02c0ded2bc1f1305fb0fa" + "ac5e6c03ee3a1924234985427b6167ca569d13df435cf8c94d1" + "9817945c5120fa5b6e83079a878e499e2e52a76a7739e9de409" + "86a8e3bd8a68ce316cee50b210000012055139900ea2ae7db9d" + "4ef0d5d4015d2d993d0590ad32662bda94daba74a5e13411aef" + "4de6f847e9e4300e5c8c36aa8e5f9032d25fd8ca01abd58c7e9" + "528677e4") + self.doit() + def compareConstructedTX(self): self.maxDiff = None self.op = operations.Bid_collateral(**{ @@ -526,7 +640,7 @@ def compareConstructedTX(self): operations=ops ) tx = tx.sign([wif], chain=prefix) - tx.verify([PrivateKey(wif).pubkey], "BTS") + tx.verify([PrivateKey(wif).pubkey], prefix) txWire = hexlify(bytes(tx)).decode("ascii") print("=" * 80) pprint(tx.json()) diff --git a/tests/test_txbuffers.py b/tests/test_txbuffers.py new file mode 100644 index 00000000..915ecdb0 --- /dev/null +++ b/tests/test_txbuffers.py @@ -0,0 +1,107 @@ +import unittest +from bitshares import BitShares +from bitsharesbase import operations +from bitshares.instance import set_shared_bitshares_instance + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + "wss://node.testnet.bitshares.eu", + nobroadcast=True, + keys={"active": wif} + ) + set_shared_bitshares_instance(self.bts) + self.bts.set_default_account("init0") + + def test_add_one_proposal_one_op(self): + bts = self.bts + tx1 = bts.new_tx() + proposal1 = bts.new_proposal(tx1, proposer="init0") + op = operations.Transfer(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "from": "1.2.0", + "to": "1.2.0", + "amount": {"amount": 0, "asset_id": "1.3.0"}, + "prefix": "TEST" + }) + proposal1.appendOps(op) + tx = tx1.json() + self.assertEqual(tx["operations"][0][0], 22) + self.assertEqual(len(tx["operations"]), 1) + ps = tx["operations"][0][1] + self.assertEqual(len(ps["proposed_ops"]), 1) + self.assertEqual(ps["proposed_ops"][0]["op"][0], 0) + + def test_add_one_proposal_two_ops(self): + bts = self.bts + tx1 = bts.new_tx() + proposal1 = bts.new_proposal(tx1, proposer="init0") + op = operations.Transfer(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "from": "1.2.0", + "to": "1.2.0", + "amount": {"amount": 0, "asset_id": "1.3.0"}, + "prefix": "TEST" + }) + proposal1.appendOps(op) + proposal1.appendOps(op) + tx = tx1.json() + self.assertEqual(tx["operations"][0][0], 22) + self.assertEqual(len(tx["operations"]), 1) + ps = tx["operations"][0][1] + self.assertEqual(len(ps["proposed_ops"]), 2) + self.assertEqual(ps["proposed_ops"][0]["op"][0], 0) + self.assertEqual(ps["proposed_ops"][1]["op"][0], 0) + + def test_have_two_proposals(self): + bts = self.bts + tx1 = bts.new_tx() + + # Proposal 1 + proposal1 = bts.new_proposal(tx1, proposer="init0") + op = operations.Transfer(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "from": "1.2.0", + "to": "1.2.0", + "amount": {"amount": 0, "asset_id": "1.3.0"}, + "prefix": "TEST" + }) + for i in range(0, 3): + proposal1.appendOps(op) + + # Proposal 1 + proposal2 = bts.new_proposal(tx1, proposer="init0") + op = operations.Transfer(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "from": "1.2.0", + "to": "1.2.0", + "amount": {"amount": 5555555, "asset_id": "1.3.0"}, + "prefix": "TEST" + }) + for i in range(0, 2): + proposal2.appendOps(op) + tx = tx1.json() + + self.assertEqual(len(tx["operations"]), 2) # 2 proposals + + # Test proposal 1 + prop = tx["operations"][0] + self.assertEqual(prop[0], 22) + ps = prop[1] + self.assertEqual(len(ps["proposed_ops"]), 3) + for i in range(0, 3): + self.assertEqual(ps["proposed_ops"][i]["op"][0], 0) + + # Test proposal 2 + prop = tx["operations"][1] + self.assertEqual(prop[0], 22) + ps = prop[1] + self.assertEqual(len(ps["proposed_ops"]), 2) + for i in range(0, 2): + self.assertEqual(ps["proposed_ops"][i]["op"][0], 0) diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 00000000..1629bda9 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,6 @@ +from bitshares.utils import assets_from_string + + +def test_assets_from_string(): + assert assets_from_string('USD:BTS') == ['USD', 'BTS'] + assert assets_from_string('BTSBOTS.S1:BTS') == ['BTSBOTS.S1', 'BTS'] diff --git a/tests/test_wallet.py b/tests/test_wallet.py new file mode 100644 index 00000000..07c46402 --- /dev/null +++ b/tests/test_wallet.py @@ -0,0 +1,27 @@ +import unittest +import mock +from pprint import pprint +from bitshares import BitShares +from bitshares.account import Account +from bitshares.amount import Amount +from bitshares.asset import Asset +from bitshares.instance import set_shared_bitshares_instance +from bitsharesbase.operationids import getOperationNameForId + +wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" + + +class Testcases(unittest.TestCase): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.bts = BitShares( + nobroadcast=True, + # We want to bundle many operations into a single transaction + bundle=True, + # Overwrite wallet to use this list of wifs only + wif=[wif] + ) + self.bts.set_default_account("init0") + set_shared_bitshares_instance(self.bts) diff --git a/tox.ini b/tox.ini index f0d19664..49ea0896 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ skip_missing_interpreters = true deps=-rrequirements-test.txt commands= coverage run -a setup.py test - coverage report --show-missing + coverage report --show-missing --ignore-errors coverage html -i [testenv:lint]