From ac9c4a67793a8538fac30b7257c45e5da4415666 Mon Sep 17 00:00:00 2001 From: Zapata Date: Wed, 26 Jul 2017 20:43:14 +0200 Subject: [PATCH 01/57] Fix market symbol split regexp. Currently code fails with asset names containing numbers like `BTS:BTSBOTS.S1`. In the ASCII table, between '/' and ':' there is all the digits... ``` _get_assets_from_string(`BTS:BTSBOTS.S1`) = ['BTS', 'BTSBOTS.S', ''] ``` --- bitshares/market.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitshares/market.py b/bitshares/market.py index fa95ad16..4620ea16 100644 --- a/bitshares/market.py +++ b/bitshares/market.py @@ -40,7 +40,7 @@ class Market(dict): quote** and obtain/pay **only base**. """ - market_sep_regex = "[/-:]" + market_sep_regex = "[/\-:]" def __init__( self, From d92cf3e83d96b130df377ee65756dc9d4ca8846f Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 7 Jul 2017 13:18:54 +0200 Subject: [PATCH 02/57] [fix] paths need to be escaped in doc --- bitshares/storage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bitshares/storage.py b/bitshares/storage.py index eb346237..5f3f20df 100644 --- a/bitshares/storage.py +++ b/bitshares/storage.py @@ -27,8 +27,8 @@ class DataDir(object): **Windows:** - * `C:\Documents and Settings\\Application Data\Local Settings\\` - * `C:\Documents and Settings\\Application Data\\` + * `C:\\Documents and Settings\\\\Application Data\\Local Settings\\\\` + * `C:\\Documents and Settings\\\\Application Data\\\\` **Linux:** From 7771b1137095a601e88713e9ed41beb34d3e96e0 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 7 Jul 2017 13:19:16 +0200 Subject: [PATCH 03/57] [fix] account history with only_ops and exclude_ops --- bitshares/account.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bitshares/account.py b/bitshares/account.py index 6514e32f..c014526b 100644 --- a/bitshares/account.py +++ b/bitshares/account.py @@ -170,6 +170,7 @@ def history( :param array only_ops: Limit generator by these operations (*optional*) :param array exclude_ops: Exclude thse operations from generator (*optional*) """ + from bitsharesbase.operations import getOperationNameForId _limit = 100 cnt = 0 @@ -197,9 +198,9 @@ def history( api="history" ) for i in txs: - if exclude_ops and i[1]["op"][0] in exclude_ops: + if exclude_ops and getOperationNameForId(i["op"][0]) in exclude_ops: continue - if not only_ops or i[1]["op"][0] in only_ops: + if not only_ops or getOperationNameForId(i["op"][0]) in only_ops: cnt += 1 yield i if limit >= 0 and cnt >= limit: From 97ad50ef7c5f46ae08756f7295946a246e376b4d Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 7 Jul 2017 13:20:04 +0200 Subject: [PATCH 04/57] [improvements] do not look for keys multiple times when signing --- bitshares/bitshares.py | 1 + bitshares/transactionbuilder.py | 31 +++++++++++++++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index 59ec2e56..dc106727 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -196,6 +196,7 @@ def finalizeOp(self, ops, account, permission): elif self.bundle: # In case we want to add more ops to the tx (bundle) self.txbuffer.appendSigner(account, permission) + return self.txbuffer.json() else: # default behavior: sign + broadcast self.txbuffer.appendSigner(account, permission) diff --git a/bitshares/transactionbuilder.py b/bitshares/transactionbuilder.py index 9e42d10e..72d1ed22 100644 --- a/bitshares/transactionbuilder.py +++ b/bitshares/transactionbuilder.py @@ -40,7 +40,6 @@ 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 """ - def fetchkeys(account, perm, level=0): if level > 2: return [] @@ -60,20 +59,23 @@ def fetchkeys(account, perm, level=0): assert permission in ["active", "owner"], "Invalid permission" - # is the account an instance of public key? - if isinstance(account, PublicKey): - self.wifs.append( - self.bitshares.wallet.getPrivateKeyForPublicKey( - str(account) + if account not in self.available_signers: + # is the account an instance of public key? + if isinstance(account, PublicKey): + self.wifs.append( + self.bitshares.wallet.getPrivateKeyForPublicKey( + str(account) + ) ) - ) - else: - account = Account(account, bitshares_instance=self.bitshares) - required_treshold = account[permission]["weight_threshold"] - keys = fetchkeys(account, permission) - if permission != "owner": - keys.extend(fetchkeys(account, "owner")) - self.wifs.extend([x[0] for x in keys]) + else: + account = Account(account, bitshares_instance=self.bitshares) + required_treshold = account[permission]["weight_threshold"] + keys = fetchkeys(account, permission) + if permission != "owner": + keys.extend(fetchkeys(account, "owner")) + self.wifs.extend([x[0] for x in keys]) + + self.available_signers.append(account) def appendWif(self, wif): """ Add a wif that should be used for signing of the transaction. @@ -202,6 +204,7 @@ def clear(self): self.ops = [] self.wifs = [] self.pop("signatures", None) + self.available_signers = [] super(TransactionBuilder, self).__init__({}) def addSigningInformation(self, account, permission): From e37aba5cfdaadf1fcdc5c541538b77542d297fe6 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Wed, 12 Jul 2017 10:41:19 +0200 Subject: [PATCH 05/57] [operations] create_worker_operation added --- bitsharesbase/objects.py | 36 +++++++++++++++++++++++++++++ bitsharesbase/operations.py | 23 ++++++++++++++++++- tests/test_transactions.py | 45 ++++++++++++++++++++++++++++++++----- 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/bitsharesbase/objects.py b/bitsharesbase/objects.py index 29b2503b..dc357750 100644 --- a/bitsharesbase/objects.py +++ b/bitsharesbase/objects.py @@ -230,3 +230,39 @@ def __init__(self, *args, **kwargs): ('description', String(kwargs["description"])), ('extensions', Set([])), ])) + + +class Vesting_balance_worker_initializer(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([ + ('pay_vesting_period_days', Uint16(kwargs["pay_vesting_period_days"])), + ])) + + +class Burn_worker_initializer(GrapheneObject): + def __init__(self, kwargs): + super().__init__(OrderedDict([])) + + +class Refund_worker_initializer(GrapheneObject): + def __init__(self, kwargs): + super().__init__(OrderedDict([])) + + +class Worker_initializer(Static_variant): + def __init__(self, o): + id = o[0] + if id == 0: + data = Refund_worker_initializer(o[1]) + elif id == 1: + data = Vesting_balance_worker_initializer(o[1]) + elif id == 2: + data = Burn_worker_initializer(o[1]) + else: + raise Exception("Unknown Worker_initializer") + super().__init__(data, id) diff --git a/bitsharesbase/operations.py b/bitsharesbase/operations.py index d696702f..688ee227 100644 --- a/bitsharesbase/operations.py +++ b/bitsharesbase/operations.py @@ -19,7 +19,8 @@ Permission, AccountOptions, AssetOptions, - ObjectId + ObjectId, + Worker_initializer, ) default_prefix = "BTS" @@ -420,3 +421,23 @@ def __init__(self, *args, **kwargs): ('amount_to_reserve', Asset(kwargs["amount_to_reserve"])), ('extensions', Set([])), ])) + + +class Worker_create(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"])), + ('owner', ObjectId(kwargs["owner"], "account")), + ('work_begin_date', PointInTime(kwargs["work_begin_date"])), + ('work_end_date', PointInTime(kwargs["work_end_date"])), + ('daily_pay', Uint64(kwargs["daily_pay"])), + ('name', String(kwargs["name"])), + ('url', String(kwargs["url"])), + ('initializer', Worker_initializer(kwargs["initializer"])), + ])) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index 4cc4a5a0..ece62b9b 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -619,14 +619,47 @@ def test_asset_reserve(self): "40c241db9cad86e27369d0e5a76b5832d585505ff177d") self.assertEqual(compare[:-130], txWire[:-130]) + def test_worker_create(self): + op = operations.Worker_create(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "owner": "1.2.0", + "work_begin_date": "1970-01-01T00:00:00", + "work_end_date": "1970-01-01T00:00:00", + "daily_pay": 0, + "name": "Myname", + "url": "myURL", + "initializer": [ + 1, {"pay_vesting_period_days": 125} + ] + }) + ops = [Operation(op)] + tx = Signed_Transaction(ref_block_num=ref_block_num, + ref_block_prefix=ref_block_prefix, + expiration=expiration, + operations=ops) + tx = tx.sign([wif], chain=prefix) + tx.verify([PrivateKey(wif).pubkey], "BTS") + txWire = hexlify(bytes(tx)).decode("ascii") + compare = ("f68585abf4dce7c80457010f00000000000000000000d202964" + "900000000000000011f75065cb1155bfcaabaf55d3357d69679" + "c7c1fe589b6dc0919fe1dde1a305009c360823a40c28907299a" + "40c241db9cad86e27369d0e5a76b5832d585505ff177d") + self.assertEqual(compare[:-130], txWire[:-130]) + def compareConstructedTX(self): # def test_online(self): # self.maxDiff = None - op = operations.Asset_reserve(**{ + op = operations.Worker_create(**{ "fee": {"amount": 0, "asset_id": "1.3.0"}, - "payer": "1.2.0", - "amount_to_reserve": {"amount": 1234567890, "asset_id": "1.3.0"}, - "extensions": [] + "owner": "1.2.0", + "work_begin_date": "1970-01-01T00:00:00", + "work_end_date": "1970-01-01T00:00:00", + "daily_pay": 0, + "name": "Myname", + "url": "myURL", + "initializer": [ + 1, {"pay_vesting_period_days": 125} + ] }) ops = [Operation(op)] tx = Signed_Transaction( @@ -645,8 +678,8 @@ def compareConstructedTX(self): from grapheneapi.grapheneapi import GrapheneAPI rpc = GrapheneAPI("localhost", 8092) compare = rpc.serialize_transaction(tx.json()) - print(compare[:-130]) - print(txWire[:-130]) + print("soll: %s" % compare[:-130]) + print("ist: %s" % txWire[:-130]) print(txWire[:-130] == compare[:-130]) self.assertEqual(compare[:-130], txWire[:-130]) From 6592ab56417c3d5e1b7963780289b6ce9c8ed66f Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Wed, 12 Jul 2017 11:02:37 +0200 Subject: [PATCH 06/57] [docs+call] worker_create --- bitshares/bitshares.py | 68 ++++++++++++++++++++++++++++++++++- bitsharesbase/transactions.py | 2 +- docs/index.rst | 1 + docs/wallet.rst | 1 + 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index dc106727..a641fdff 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -866,9 +866,10 @@ def vesting_balance_withdraw(self, vesting_id, amount=None, account=None): """ Withdraw vesting balance :param str vesting_id: Id of the vesting object - :param bitshares.amount.Amount Amount to withdraw ("all" if not provided") + :param bitshares.amount.Amount Amount: to withdraw ("all" if not provided") :param str account: (optional) the account to allow access to (defaults to ``default_account``) + """ if not account: if "default_account" in config: @@ -1103,3 +1104,68 @@ def reserve(self, amount, account=None): "extensions": [] }) return self.finalizeOp(op, account, "active") + + def create_worker( + self, + name, + daily_pay, + end, + url="", + begin=None, + payment_type="vesting", + pay_vesting_period_days=0, + account=None + ): + """ Reserve/Burn an amount of this shares + + This removes the shares from the supply + + **Required** + + :param str name: Name of the worke + :param bitshares.amount.Amount daily_pay: The amount to be paid daily + :param datetime end: Date/time of end of the worker + + **Optional** + + :param str url: URL to read more about the worker + :param datetime begin: Date/time of begin of the worker + :param string payment_type: ["burn", "refund", "vesting"] (default: "vesting") + :param int pay_vesting_period_days: Days of vesting (default: 0) + :param str account: (optional) the account to allow access + to (defaults to ``default_account``) + """ + from bitsharesbase.transactions import timeformat + assert isinstance(daily_pay, Amount) + assert daily_pay["symbol"] == "BTS" + if not begin: + begin = datetime.utcnow() + 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) + + if payment_type == "refund": + initializer = [0, {}] + elif payment_type == "vesting": + initializer = [ + 1, {"pay_vesting_period_days": pay_vesting_period_days} + ] + elif payment_type == "burn": + initializer = [2, {}] + else: + raise ValueError('payment_type not in ["burn", "refund", "vesting"]') + + op = operations.Worker_create(**{ + "fee": {"amount": 0, "asset_id": "1.3.0"}, + "owner": account["id"], + "work_begin_date": begin.strftime(timeformat), + "work_end_date": end.strftime(timeformat), + "daily_pay": int(daily_pay), + "name": name, + "url": url, + "initializer": initializer + }) + return self.finalizeOp(op, account, "active") diff --git a/bitsharesbase/transactions.py b/bitsharesbase/transactions.py index 30586544..9b47176f 100644 --- a/bitsharesbase/transactions.py +++ b/bitsharesbase/transactions.py @@ -16,7 +16,7 @@ Account_create, ) from .objects import Asset -from graphenebase.transactions import getBlockParams, formatTimeFromNow +from graphenebase.transactions import getBlockParams, formatTimeFromNow, timeformat def addRequiredFees(ws, ops, asset_id="1.3.0"): diff --git a/docs/index.rst b/docs/index.rst index 763fd59d..98dd5256 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -102,6 +102,7 @@ General installation quickstart tutorials + configuration contribute support diff --git a/docs/wallet.rst b/docs/wallet.rst index e46d3be4..beeef88d 100644 --- a/docs/wallet.rst +++ b/docs/wallet.rst @@ -33,6 +33,7 @@ A private key can be added by using the **after** unlocking the wallet with the correct passphrase: .. code-block:: python + from bitshares import BitShares bitshares = BitShares() bitshares.wallet.unlock("supersecret-passphrase") From cbdf82483b44dfde0e5b852af4847a9b389d8649 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Wed, 2 Aug 2017 12:51:23 +0200 Subject: [PATCH 07/57] [asset] list settlements and call positions of an asset --- bitshares/asset.py | 75 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/bitshares/asset.py b/bitshares/asset.py index 5fa701bc..41c3183d 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -71,8 +71,8 @@ def refresh(self): self["dynamic_asset_data"] = self.bitshares.rpc.get_object(asset["dynamic_asset_data_id"]) # Permissions and flags - self["permissions"] = todict(asset["options"]["issuer_permissions"]) - self["flags"] = todict(asset["options"]["flags"]) + self["permissions"] = todict(asset["options"].get("issuer_permissions")) + self["flags"] = todict(asset["options"].get("flags")) try: self["description"] = json.loads(asset["options"]["description"]) except: @@ -122,11 +122,78 @@ def feeds(self): @property def feed(self): from .price import PriceFeed + assert self.is_bitasset self.ensure_full() - if not self.is_bitasset: - return return PriceFeed(self["bitasset_data"]["current_feed"]) + @property + def calls(self): + return self.get_call_positions(10) + + def get_call_orders(self, limit=100): + from .price import Price + from .amount import Amount + assert limit <= 100 + assert self.is_bitasset + self.ensure_full() + r = list() + bitasset = self["bitasset_data"] + settlement_price = Price(bitasset["current_feed"]["settlement_price"]) + ret = self.bitshares.rpc.get_call_orders(self["id"], limit) + for call in ret[:limit]: + call_price = Price(call["call_price"]) + collateral_amount = Amount( + { + "amount": call["collateral"], + "asset_id": call["call_price"]["base"]["asset_id"] + }, + bitshares_instance=self.bitshares + ) + debt_amount = Amount( + { + "amount": call["debt"], + "asset_id": call["call_price"]["quote"]["asset_id"], + }, + bitshares_instance=self.bitshares + ) + r.append({ + "account": Account( + call["borrower"], + lazy=True, + bitshares_instance=self.bitshares + ), + "collateral": collateral_amount, + "debt": debt_amount, + "call_price": call_price, + "settlement_price": settlement_price, + "ratio": float(collateral_amount) / float(debt_amount) * float(settlement_price) + }) + return r + + @property + def settlements(self): + return self.get_settle_orders(10) + + def get_settle_orders(self, limit=100): + from .amount import Amount + from .utils import formatTimeString + assert limit <= 100 + assert self.is_bitasset + r = list() + ret = self.bitshares.rpc.get_settle_orders(self["id"], limit) + for settle in ret[:limit]: + r.append({ + "account": Account( + settle["owner"], + lazy=True, + bitshares_instance=self.bitshares + ), + "amount": Amount(settle["balance"], + bitshares_instance=self.bitshares), + "date": formatTimeString(settle["settlement_date"]) + }) + return r + def __getitem__(self, key): if not self.cached: self.refresh() From 2a66c8be39b5bcd62115d33ed80a356fbe0d40db Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Thu, 3 Aug 2017 14:16:18 +0200 Subject: [PATCH 08/57] [setup] update dependency on graphenelib --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 49cb305b..e56e188a 100755 --- a/setup.py +++ b/setup.py @@ -40,12 +40,12 @@ 'Topic :: Office/Business :: Financial', ], install_requires=[ - "graphenelib==0.5.2", + "graphenelib>=0.5.3", "websockets", "appdirs", "Events", "scrypt", - "pycrypto", # for AES + "pycrypto", # for AES, installed through graphenelib already ], setup_requires=['pytest-runner'], tests_require=['pytest'], From cd64add2a0d96bd8edb6bb94ed99392cb791c6b9 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Thu, 3 Aug 2017 14:16:38 +0200 Subject: [PATCH 09/57] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e56e188a..f1ee0049 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.7' +VERSION = '0.1.8' setup( name='bitshares', From 40188154f07c6236fe610b44fdd05371ea748b55 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Thu, 3 Aug 2017 14:26:00 +0200 Subject: [PATCH 10/57] [tests] fix worker_creation --- tests/test_transactions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index ece62b9b..3d034802 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -640,10 +640,11 @@ def test_worker_create(self): tx = tx.sign([wif], chain=prefix) tx.verify([PrivateKey(wif).pubkey], "BTS") txWire = hexlify(bytes(tx)).decode("ascii") - compare = ("f68585abf4dce7c80457010f00000000000000000000d202964" - "900000000000000011f75065cb1155bfcaabaf55d3357d69679" - "c7c1fe589b6dc0919fe1dde1a305009c360823a40c28907299a" - "40c241db9cad86e27369d0e5a76b5832d585505ff177d") + compare = ("f68585abf4dce7c804570122000000000000000000000000000" + "0000000000000000000000000064d796e616d65056d7955524c" + "017d0000012049a1430c8045ce7e7a3c0882f537aa9d4547fca" + "65a6c17967c5daf47aad383175e9f95d0187398da8b8f5b4c78" + "561f4427b0fc8758e4a3a92afab9388f849f5a") self.assertEqual(compare[:-130], txWire[:-130]) def compareConstructedTX(self): From b19aae1aca63b454c4ae756ddf045d99a7a2d8f5 Mon Sep 17 00:00:00 2001 From: grcgrc Date: Sun, 1 Oct 2017 15:33:12 +0100 Subject: [PATCH 11/57] pycryptodome & readme updates pycrypto has been depreciated & replaced with pycryptodome. --- README.md | 29 ++++++++++++++++++++++++++--- bitsharesbase/memo.py | 2 +- docs/requirements.txt | 4 ++-- requirements-test.txt | 4 ++-- setup.py | 2 +- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 1584d555..9e9ee349 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 python-dev3 +$ 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 +``` \ No newline at end of file diff --git a/bitsharesbase/memo.py b/bitsharesbase/memo.py index c1045527..d75fa9f9 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 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..d8696895 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,7 +1,7 @@ graphenelib -pycrypto==2.6.1 +pycryptodome==3.4.6 scrypt==0.7.1 Events==0.2.2 pyyaml pytest -coverage +coverage \ No newline at end of file diff --git a/setup.py b/setup.py index f1ee0049..588ac31d 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ "appdirs", "Events", "scrypt", - "pycrypto", # for AES, installed through graphenelib already + "pycryptodome", # for AES, installed through graphenelib already ], setup_requires=['pytest-runner'], tests_require=['pytest'], From 397098df83c26c392ba5c494e34942577fa91c28 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Wed, 11 Oct 2017 14:07:36 +0200 Subject: [PATCH 12/57] [peerplays] refresh consistency between libraries --- bitshares/account.py | 8 +-- bitshares/asset.py | 12 +++- bitshares/bitshares.py | 51 ++++++++++++++++- bitshares/blockchain.py | 5 +- bitshares/blockchainobject.py | 4 +- bitshares/committee.py | 2 +- bitshares/memo.py | 2 +- bitshares/proposal.py | 4 ++ bitshares/transactionbuilder.py | 99 ++++++++++++++++++++------------- bitshares/wallet.py | 2 - 10 files changed, 134 insertions(+), 55 deletions(-) diff --git a/bitshares/account.py b/bitshares/account.py index 3871f1b0..25efca5a 100644 --- a/bitshares/account.py +++ b/bitshares/account.py @@ -38,15 +38,15 @@ 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, + lazy=lazy, + full=full, bitshares_instance=None ) @@ -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( diff --git a/bitshares/asset.py b/bitshares/asset.py index ec42f633..9682d71d 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -38,8 +38,8 @@ def __init__( self.full = full super().__init__( asset, - lazy=False, - full=False, + lazy=lazy, + full=full, bitshares_instance=None ) @@ -66,6 +66,14 @@ def refresh(self): except: self["description"] = asset["options"]["description"] + @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`? diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index 7fbd7cc4..04723b89 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -126,6 +126,9 @@ def __init__(self, self.bundle = bool(kwargs.get("bundle", False)) self.blocking = kwargs.get("blocking", False) + # Multiple txbuffers can be stored here + self._txbuffers = [] + # Store config for access through other Classes self.config = config @@ -136,8 +139,11 @@ def __init__(self, **kwargs) self.wallet = Wallet(self.rpc, **kwargs) - self.txbuffer = TransactionBuilder(bitshares_instance=self) + self.new_txbuffer() + # ------------------------------------------------------------------------- + # Basic Calls + # ------------------------------------------------------------------------- def connect(self, node="", rpcuser="", @@ -186,6 +192,10 @@ 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) @@ -238,6 +248,42 @@ def info(self): """ return self.rpc.get_dynamic_global_properties() + # ------------------------------------------------------------------------- + # Transaction Buffers + # ------------------------------------------------------------------------- + @property + def txbuffer(self): + """ Returns the currently active tx buffer + """ + return self._txbuffers[self._current_txbuffer] + + def set_txbuffer(self, i): + """ Lets you switch the current txbuffer + + :param int i: Id of the txbuffer + """ + self._current_txbuffer = i + + def get_txbuffer(self, i): + """ Returns the txbuffer with id i + """ + if i < len(self._txbuffers): + return self._txbuffers[i] + + def new_txbuffer(self, *args, **kwargs): + """ Let's obtain a new txbuffer + + :returns int txid: id of the new txbuffer + """ + self._txbuffers.append(TransactionBuilder( + *args, + bitshares_instance=self, + **kwargs + )) + id = len(self._txbuffers) - 1 + self.set_txbuffer(id) + return id + def create_account( self, account_name, @@ -620,6 +666,9 @@ def update_memo_key(self, key, account=None): }) return self.finalizeOp(op, account["name"], "active") + # ------------------------------------------------------------------------- + # Approval and Disapproval of witnesses, workers, committee, and proposals + # ------------------------------------------------------------------------- def approvewitness(self, witnesses, account=None): """ Approve a witness diff --git a/bitshares/blockchain.py b/bitshares/blockchain.py index 1d614f69..a867e771 100644 --- a/bitshares/blockchain.py +++ b/bitshares/blockchain.py @@ -190,9 +190,8 @@ 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"]): diff --git a/bitshares/blockchainobject.py b/bitshares/blockchainobject.py index ce089961..d4b43757 100644 --- a/bitshares/blockchainobject.py +++ b/bitshares/blockchainobject.py @@ -56,7 +56,7 @@ def __init__( klass=None, space_id=1, object_id=None, - lazy=True, + lazy=False, use_cache=True, bitshares_instance=None, **kwargs @@ -117,7 +117,7 @@ def testid(self, id): 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): diff --git a/bitshares/committee.py b/bitshares/committee.py index d214f2b3..ca171188 100644 --- a/bitshares/committee.py +++ b/bitshares/committee.py @@ -24,4 +24,4 @@ def refresh(self): @property def account(self): - return Account(self.member) + return Account(self.identifier) diff --git a/bitshares/memo.py b/bitshares/memo.py index 5d74cf14..b33e6bdd 100644 --- a/bitshares/memo.py +++ b/bitshares/memo.py @@ -97,7 +97,7 @@ def decrypt(self, memo): PrivateKey(memo_wif), PublicKey( self.from_account["options"]["memo_key"], - prefix=self.peerplays.rpc.chain_params["prefix"] + prefix=self.bitshares.rpc.chain_params["prefix"] ), memo.get("nonce"), memo.get("message") diff --git a/bitshares/proposal.py b/bitshares/proposal.py index 186e0c21..5ad88605 100644 --- a/bitshares/proposal.py +++ b/bitshares/proposal.py @@ -21,6 +21,10 @@ def refresh(self): raise ProposalDoesNotExistException super(Proposal, self).__init__(proposal[0]) + @property + def proposed_operations(self): + yield from self["proposed_transaction"]["operations"] + class Proposals(list): """ Obtain a list of pending proposals for an account diff --git a/bitshares/transactionbuilder.py b/bitshares/transactionbuilder.py index c6f51e88..730398e0 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 @@ -18,13 +17,34 @@ 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_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 unset_require_reconstruction(self): + self._require_reconstruction = False def appendOps(self, ops): """ Append op(s) to the transaction builder @@ -35,30 +55,35 @@ 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"] + 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]) + wif = self.bitshares.wallet.getPrivateKeyForPublicKey( + authority[0]) if wif: r.append([wif, authority[1]]) 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: # is the account an instance of public key? if isinstance(account, PublicKey): @@ -112,14 +137,16 @@ def constructTx(self): 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) @@ -143,7 +170,8 @@ def sign(self): # 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 +196,42 @@ 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"]: + if not self.is_signed(): self.sign() + 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 - - 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 + try: + if self.bitshares.blocking: + ret = self.bitshares.rpc.broadcast_transaction_synchronous( + ret, api="network_broadcast") + else: + self.bitshares.rpc.broadcast_transaction( + ret, api="network_broadcast") + except Exception as e: + raise e - """ # 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 = [] + # This makes sure that is_constructed will return False afterwards + self["expiration"] = None super(TransactionBuilder, self).__init__({}) def addSigningInformation(self, account, permission): @@ -258,6 +277,8 @@ def addSigningInformation(self, account, permission): 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 appendMissingSignatures(self): diff --git a/bitshares/wallet.py b/bitshares/wallet.py index c9b6fbfb..355d93b1 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -1,9 +1,7 @@ import logging import os - from graphenebase import bip38 from bitsharesbase.account import PrivateKey, GPHPrivateKey - from .account import Account from .exceptions import ( InvalidWifError, From 105a16101895a99bcc3840e39358e10dae5baff1 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Mon, 16 Oct 2017 13:33:42 +0200 Subject: [PATCH 13/57] [fund_fee_pool] new call --- bitshares/bitshares.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index 04723b89..d6d21bf3 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -1101,7 +1101,7 @@ def upgrade_account(self, 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.Account_upgrade(**{ "fee": {"amount": 0, "asset_id": "1.3.0"}, "account_to_upgrade": account["id"], @@ -1144,7 +1144,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"], @@ -1195,7 +1195,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, {}] @@ -1219,3 +1219,28 @@ def create_worker( "initializer": initializer }) return self.finalizeOp(op, account, "active") + + def fund_fee_pool(self, symbol, amount, account=None): + """ 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") From 5b00f43b6cb94bc20c31512c48e1cef436075530 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 10 Nov 2017 10:42:14 +0100 Subject: [PATCH 14/57] [worker] Allow to also list all workers --- bitshares/worker.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/bitshares/worker.py b/bitshares/worker.py index bfdbbfa8..15ef8ddb 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 @@ -31,13 +32,18 @@ def account(self): 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) + self.workers = self.bitshares.rpc.get_workers_by_account( + account["id"]) + else: + self.workers = self.bitshares.rpc.get_all_workers() super(Workers, self).__init__( [ From 05a77795c38dd02e7b57ce726c22efbd9b28d976 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 10 Nov 2017 10:43:17 +0100 Subject: [PATCH 15/57] [transactionbuilder] ensure that operations are also available when waiting for tx to be included into a block --- bitshares/transactionbuilder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bitshares/transactionbuilder.py b/bitshares/transactionbuilder.py index 730398e0..fdae81ee 100644 --- a/bitshares/transactionbuilder.py +++ b/bitshares/transactionbuilder.py @@ -215,6 +215,7 @@ def broadcast(self): 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") From 6182f957a75deba72164b7541a5cd4c44523ea0e Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 10 Nov 2017 10:43:30 +0100 Subject: [PATCH 16/57] [fix] minor fixes and assertions --- bitshares/amount.py | 6 ++++-- bitshares/bitshares.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) 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/bitshares.py b/bitshares/bitshares.py index d6d21bf3..f974c5b8 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -1050,6 +1050,8 @@ 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`!" if not account: if "default_account" in config: From ff7178c1a178ad82e4e35dbe5d80170da7c44a70 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 14 Nov 2017 11:43:18 +0100 Subject: [PATCH 17/57] [blockchainobjects] improve testing for objects ids --- bitshares/blockchainobject.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/bitshares/blockchainobject.py b/bitshares/blockchainobject.py index d4b43757..d47df3a6 100644 --- a/bitshares/blockchainobject.py +++ b/bitshares/blockchainobject.py @@ -52,19 +52,35 @@ class BlockchainObject(dict): def __init__( self, data, - *args, klass=None, space_id=1, object_id=None, lazy=False, use_cache=True, bitshares_instance=None, + *args, **kwargs ): self.bitshares = bitshares_instance or shared_bitshares_instance() self.cached = False self.identifier = None + def test_valid_objectid(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 + + # 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 +98,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 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: From e64523e10c52fdddd73a3a7da3816077f1662ba9 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 14 Nov 2017 11:43:30 +0100 Subject: [PATCH 18/57] [price] fix multiplication of prices --- bitshares/price.py | 24 +++++++++++++++++------- tests/test_price.py | 10 +++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/bitshares/price.py b/bitshares/price.py index 5e4938f6..f94f9002 100644 --- a/bitshares/price.py +++ b/bitshares/price.py @@ -238,14 +238,24 @@ 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"]) + a["quote"] = Amount(float(self["quote"]) * float(other["quote"]), other["quote"]["symbol"]) + # 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"]) + a["quote"] = Amount(float(self["quote"]) * float(other["quote"]), self["quote"]["symbol"]) + else: + raise ValueError("Wrong rotation of prices") elif isinstance(other, Amount): assert other["asset"]["id"] == self["quote"]["asset"]["id"] a = other.copy() * self["price"] diff --git a/tests/test_price.py b/tests/test_price.py index 9c12e114..4353a1d6 100644 --- a/tests/test_price.py +++ b/tests/test_price.py @@ -35,14 +35,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 +50,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") From 5f129ed3f3a046e8a17aa70e1ac345de715d4d4b Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 14 Nov 2017 11:46:17 +0100 Subject: [PATCH 19/57] [memo] linting fix --- bitsharesbase/memo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitsharesbase/memo.py b/bitsharesbase/memo.py index d75fa9f9..df8f584a 100644 --- a/bitsharesbase/memo.py +++ b/bitsharesbase/memo.py @@ -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) From 5faa4b4675de46d496d82ebd7f1ac2f72876f5ef Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Thu, 7 Dec 2017 13:44:43 +0100 Subject: [PATCH 20/57] [tests+consolidation] unit tests and consolidation with pypeerplays --- .gitignore | 2 + .travis.yml | 5 +- bitshares/account.py | 4 +- bitshares/asset.py | 2 +- bitshares/bitshares.py | 575 ++++++++++++++++++++------------ bitshares/block.py | 6 +- bitshares/blockchainobject.py | 60 ++-- bitshares/committee.py | 26 +- bitshares/exceptions.py | 50 +-- bitshares/instance.py | 27 +- bitshares/price.py | 2 - bitshares/transactionbuilder.py | 222 +++++++++--- bitshares/utils.py | 33 +- bitshares/wallet.py | 61 +++- bitshares/witness.py | 27 +- bitsharesbase/objects.py | 14 +- bitsharesbase/operations.py | 8 +- requirements-test.txt | 3 +- setup.py | 2 +- tests/.ropeproject/globalnames | 4 + tests/.ropeproject/history | 1 + tests/.ropeproject/objectdb | 1 + tests/__init__.py | 0 tests/test_account.py | 71 ++++ tests/test_aes.py | 51 +++ tests/test_amount.py | 225 +++++++++++++ tests/test_asset.py | 39 +++ tests/test_base_objects.py | 31 ++ tests/test_bitshares.py | 227 +++++++++++++ tests/test_objectcache.py | 33 ++ tests/test_price.py | 3 +- tests/test_proposals.py | 123 +++++++ tests/test_transactions.py | 10 +- tests/test_txbuffers.py | 107 ++++++ tests/test_wallet.py | 27 ++ tox.ini | 2 +- 36 files changed, 1699 insertions(+), 385 deletions(-) create mode 100644 tests/.ropeproject/globalnames create mode 100644 tests/.ropeproject/history create mode 100644 tests/.ropeproject/objectdb create mode 100644 tests/__init__.py create mode 100644 tests/test_account.py create mode 100644 tests/test_aes.py create mode 100644 tests/test_amount.py create mode 100644 tests/test_asset.py create mode 100644 tests/test_base_objects.py create mode 100644 tests/test_bitshares.py create mode 100644 tests/test_objectcache.py create mode 100644 tests/test_proposals.py create mode 100644 tests/test_txbuffers.py create mode 100644 tests/test_wallet.py 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/bitshares/account.py b/bitshares/account.py index 25efca5a..a646f7d1 100644 --- a/bitshares/account.py +++ b/bitshares/account.py @@ -165,7 +165,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 +191,7 @@ def history( cnt += 1 yield i if limit >= 0 and cnt >= limit: - raise StopIteration + return if not txs: break diff --git a/bitshares/asset.py b/bitshares/asset.py index 9682d71d..a4c83dd5 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -48,7 +48,7 @@ def refresh(self): """ asset = self.bitshares.rpc.get_asset(self.identifier) if not asset: - raise AssetDoesNotExistsException + raise AssetDoesNotExistsException(self.identifier) super(Asset, self).__init__(asset) if self.full: if "bitasset_data_id" in asset: diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index f974c5b8..a87eb593 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -24,7 +24,8 @@ 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 +36,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 +75,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 +95,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,14 +132,14 @@ 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) - # Multiple txbuffers can be stored here - self._txbuffers = [] + # 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 @@ -139,7 +151,9 @@ def __init__(self, **kwargs) self.wallet = Wallet(self.rpc, **kwargs) - self.new_txbuffer() + + # txbuffers/propbuffer are initialized and cleared + self.clear() # ------------------------------------------------------------------------- # Basic Calls @@ -170,20 +184,35 @@ def newWallet(self, pwd): :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 + :raises bitshares.exceptions.WalletExists: if there is already a + wallet created """ self.wallet.create(pwd) - def finalizeOp(self, ops, account, permission): + def set_default_account(self, account): + """ Set the default account to be used + """ + Account(account) + config["default_account"] = account + + 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:: @@ -197,9 +226,34 @@ def finalizeOp(self, ops, account, permission): :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) @@ -255,35 +309,144 @@ def info(self): def txbuffer(self): """ Returns the currently active tx buffer """ - return self._txbuffers[self._current_txbuffer] + return self.tx() - def set_txbuffer(self, i): - """ Lets you switch the current txbuffer + @property + def propbuffer(self): + """ Return the default proposal buffer + """ + return self.proposal() - :param int i: Id of the txbuffer + def tx(self): + """ Returns the default transaction buffer """ - self._current_txbuffer = i + return self._txbuffers[0] - def get_txbuffer(self, i): - """ Returns the txbuffer with id i + 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 i < len(self._txbuffers): - return self._txbuffers[i] + 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 - def new_txbuffer(self, *args, **kwargs): + 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 """ - self._txbuffers.append(TransactionBuilder( + builder = TransactionBuilder( *args, bitshares_instance=self, **kwargs - )) - id = len(self._txbuffers) - 1 - self.set_txbuffer(id) - return id + ) + 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.rpc.chain_params["prefix"] + }) + return self.finalizeOp(op, account, "active", **kwargs) + # ------------------------------------------------------------------------- + # Account related calls + # ------------------------------------------------------------------------- def create_account( self, account_name, @@ -300,11 +463,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 @@ -335,10 +499,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"]: @@ -379,9 +547,12 @@ 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.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"]) else: raise ValueError( "Call incomplete! Provide either a password or public keys!" @@ -409,7 +580,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"}, @@ -436,46 +608,27 @@ def create_account( "prefix": self.rpc.chain_params["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), + "account_to_upgrade": account["id"], + "upgrade_to_lifetime_member": True, "prefix": self.rpc.chain_params["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 @@ -486,14 +639,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. @@ -555,12 +712,14 @@ def allow(self, foreign, weight=None, permission="active", "prefix": self.rpc.chain_params["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. @@ -609,6 +768,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 @@ -634,11 +795,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 @@ -664,12 +825,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) # ------------------------------------------------------------------------- # Approval and Disapproval of witnesses, workers, committee, and proposals # ------------------------------------------------------------------------- - def approvewitness(self, witnesses, account=None): + def approvewitness(self, witnesses, account=None, **kwargs): """ Approve a witness :param list witnesses: list of Witness name or id @@ -704,9 +865,9 @@ def approvewitness(self, witnesses, account=None): "extensions": {}, "prefix": self.rpc.chain_params["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 @@ -742,9 +903,9 @@ def disapprovewitness(self, witnesses, account=None): "extensions": {}, "prefix": self.rpc.chain_params["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 @@ -779,9 +940,9 @@ def approvecommittee(self, committees, account=None): "extensions": {}, "prefix": self.rpc.chain_params["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 @@ -817,9 +978,95 @@ def disapprovecommittee(self, committees, account=None): "extensions": {}, "prefix": self.rpc.chain_params["prefix"] }) - return self.finalizeOp(op, account["name"], "active") + return self.finalizeOp(op, account["name"], "active", **kwargs) - def approveworker(self, workers, account=None): + def approveproposal( + self, proposal_ids, account=None, approver=None, **kwargs + ): + """ 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"], + 'active_approvals_to_add': [approver["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["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.rpc.chain_params["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 @@ -849,9 +1096,9 @@ def approveworker(self, workers, account=None): "extensions": {}, "prefix": self.rpc.chain_params["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 @@ -882,9 +1129,9 @@ def disapproveworker(self, workers, account=None): "extensions": {}, "prefix": self.rpc.chain_params["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``. @@ -910,9 +1157,9 @@ def cancel(self, orderNumbers, account=None): "order": order, "extensions": [], "prefix": self.rpc.chain_params["prefix"]})) - return self.finalizeOp(op, account["name"], "active") + 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 @@ -944,89 +1191,6 @@ def vesting_balance_withdraw(self, vesting_id, amount=None, account=None): }) 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, @@ -1092,27 +1256,7 @@ def publish_price_feed( }) 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, bitshares_instance=self) - 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"] - }) - 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 @@ -1129,9 +1273,9 @@ def update_witness(self, witness_identifier, url=None, key=None): "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 @@ -1155,7 +1299,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, @@ -1166,7 +1310,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 @@ -1220,9 +1365,9 @@ 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): + 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 @@ -1245,4 +1390,4 @@ def fund_fee_pool(self, symbol, amount, account=None): "amount": int(float(amount) * 10 ** asset["precision"]), "extensions": [] }) - return self.finalizeOp(op, account, "active") + return self.finalizeOp(op, account, "active", **kwargs) diff --git a/bitshares/block.py b/bitshares/block.py index 79ea6737..4a3af34a 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 diff --git a/bitshares/blockchainobject.py b/bitshares/blockchainobject.py index d47df3a6..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): @@ -65,21 +70,10 @@ def __init__( self.cached = False self.identifier = None - def test_valid_objectid(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 - # We don't read lists, sets, or tuples if isinstance(data, (list, set, tuple)): - raise ValueError("Cannot interpret lists! Please load elements individually!") + raise ValueError( + "Cannot interpret lists! Please load elements individually!") if klass and isinstance(data, klass): self.identifier = data.get("id") @@ -98,7 +92,7 @@ def test_valid_objectid(i): self.identifier = data else: self.identifier = data - if test_valid_objectid(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): @@ -110,6 +104,23 @@ def test_valid_objectid(i): 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: @@ -119,9 +130,11 @@ 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 @@ -150,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 ca171188..9d0cece7 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,35 @@ 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) + 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) + member = self.bitshares.rpc.get_committee_member_by_account( + account["id"]) + if not member: raise CommitteeMemberDoesNotExistsException super(Committee, self).__init__(member) - self.cached = True + self.account_id = account["id"] @property def account(self): - return Account(self.identifier) + return Account(self.account_id) diff --git a/bitshares/exceptions.py b/bitshares/exceptions.py index 2b5e8288..0443b118 100644 --- a/bitshares/exceptions.py +++ b/bitshares/exceptions.py @@ -24,68 +24,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 +94,9 @@ class WorkerDoesNotExistsException(Exception): """ Worker does not exist """ pass + + +class ObjectNotInProposalBuffer(Exception): + """ Object was not found in proposal + """ + 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/price.py b/bitshares/price.py index f94f9002..8161c45f 100644 --- a/bitshares/price.py +++ b/bitshares/price.py @@ -5,7 +5,6 @@ from .amount import Amount from .asset import Asset from .utils import formatTimeString -from .witness import Witness from .utils import parse_time @@ -393,7 +392,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() diff --git a/bitshares/transactionbuilder.py b/bitshares/transactionbuilder.py index fdae81ee..17f1a6f7 100644 --- a/bitshares/transactionbuilder.py +++ b/bitshares/transactionbuilder.py @@ -13,6 +13,116 @@ 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. @@ -31,22 +141,51 @@ def __init__( # Do we need to reconstruct the tx from self.ops? self._require_reconstruction = True - def is_signed(self): + 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): + def _is_constructed(self): return "expiration" in self and self["expiration"] - def is_require_reconstruction(self): + def _is_require_reconstruction(self): return self._require_reconstruction - def set_require_reconstruction(self): + def _set_require_reconstruction(self): self._require_reconstruction = True - def unset_require_reconstruction(self): + def _unset_require_reconstruction(self): self._require_reconstruction = False - def appendOps(self, ops): + 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 @@ -55,7 +194,7 @@ def appendOps(self, ops): self.ops.extend(ops) else: self.ops.append(ops) - self.set_require_reconstruction() + self._set_require_reconstruction() def appendSigner(self, account, permission): """ Try to obtain the wif key from the wallet by telling which account @@ -84,10 +223,10 @@ def fetchkeys(account, perm, level=0): return r - 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) ) @@ -98,9 +237,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. @@ -108,7 +248,7 @@ def appendWif(self, wif): if wif: try: PrivateKey(wif) - self.wifs.append(wif) + self.wifs.add(wif) except: raise InvalidWifError @@ -116,25 +256,19 @@ 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( @@ -146,7 +280,7 @@ def constructTx(self): operations=ops ) super(TransactionBuilder, self).__init__(self.tx.json()) - self.unset_require_reconstruction() + self._unset_require_reconstruction() def sign(self): """ Sign a provided transaction witht he provided key(s) @@ -159,12 +293,17 @@ 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 @@ -200,9 +339,13 @@ def broadcast(self): :param tx tx: Signed transaction to broadcast """ - if not self.is_signed(): + # 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: @@ -229,9 +372,9 @@ def clear(self): """ Clear the transaction builder and start from scratch """ self.ops = [] - self.wifs = [] - self.available_signers = [] - # This makes sure that is_constructed will return False afterwards + self.wifs = set() + self.signing_accounts = [] + # This makes sure that _is_constructed will return False afterwards self["expiration"] = None super(TransactionBuilder, self).__init__({}) @@ -275,13 +418,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 - """ - if not self.is_constructed() or self.is_require_reconstruction(): - self.constructTx() - 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..52e27e1e 100644 --- a/bitshares/utils.py +++ b/bitshares/utils.py @@ -1,5 +1,6 @@ import time from datetime import datetime +from .exceptions import ObjectNotInProposalBuffer timeFormat = '%Y-%m-%dT%H:%M:%S' @@ -28,10 +29,38 @@ 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 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/wallet.py b/bitshares/wallet.py index 355d93b1..8a26a0a9 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -19,7 +19,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: @@ -84,7 +85,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()) @@ -168,7 +170,8 @@ 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 @@ -185,13 +188,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 @@ -217,7 +222,8 @@ def getPrivateKeyForPublicKey(self, pub): if not self.created(): raise NoWalletException - return self.decrypt_wif(self.keyStorage.getPrivateKeyForPublicKey(pub)) + return self.decrypt_wif( + self.keyStorage.getPrivateKeyForPublicKey(pub)) def removePrivateKeyFromPublicKey(self, pub): """ Remove a key from the wallet database @@ -260,7 +266,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 @@ -286,8 +293,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 @@ -298,26 +313,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) + 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) 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 @@ -338,7 +363,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..dff4ca44 100644 --- a/bitshares/witness.py +++ b/bitshares/witness.py @@ -8,26 +8,23 @@ 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 @@ -41,11 +38,13 @@ def account(self): 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/bitsharesbase/objects.py b/bitsharesbase/objects.py index 38b8fe6a..89b9fba0 100644 --- a/bitsharesbase/objects.py +++ b/bitsharesbase/objects.py @@ -88,20 +88,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)), diff --git a/bitsharesbase/operations.py b/bitsharesbase/operations.py index 157f74ca..32a77bef 100644 --- a/bitsharesbase/operations.py +++ b/bitsharesbase/operations.py @@ -39,13 +39,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([ diff --git a/requirements-test.txt b/requirements-test.txt index d8696895..bad8ffde 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,4 +4,5 @@ scrypt==0.7.1 Events==0.2.2 pyyaml pytest -coverage \ No newline at end of file +coverage +mock diff --git a/setup.py b/setup.py index 588ac31d..3cc4ea46 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'Topic :: Office/Business :: Financial', ], install_requires=[ - "graphenelib>=0.5.3", + "graphenelib>=0.5.5", "websockets", "appdirs", "Events", 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..21e06383 --- /dev/null +++ b/tests/test_asset.py @@ -0,0 +1,39 @@ +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"]) 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..dcdf5374 --- /dev/null +++ b/tests/test_bitshares.py @@ -0,0 +1,227 @@ +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}, + ) + # 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_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 4353a1d6..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) 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..4a62a490 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -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" 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_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] From 46203796f93099772afa7a4e942ca48defbb909e Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Thu, 7 Dec 2017 14:23:47 +0100 Subject: [PATCH 21/57] [asset] ensure that assets are reloaded fully on requests --- bitshares/asset.py | 11 ++++++++++- bitshares/dex.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/bitshares/asset.py b/bitshares/asset.py index a4c83dd5..b6b8ea3e 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -67,6 +67,15 @@ def refresh(self): 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"] @@ -93,7 +102,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() diff --git a/bitshares/dex.py b/bitshares/dex.py index b5f768f2..87d5777c 100644 --- a/bitshares/dex.py +++ b/bitshares/dex.py @@ -88,6 +88,7 @@ def list_debt_positions(self, account=None): quote = Asset(debt["call_price"]["quote"]["asset_id"], full=True) if not quote.is_bitasset: continue + quote.ensure_full() bitasset = quote["bitasset_data"] settlement_price = Price(bitasset["current_feed"]["settlement_price"]) if not settlement_price: From ab8722f4a5eb4afe2704f39c365a162a6d3500e9 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 12 Dec 2017 08:49:15 +0100 Subject: [PATCH 22/57] [wallet] store the wallet passphrase even if no key is inserted --- bitshares/wallet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bitshares/wallet.py b/bitshares/wallet.py index 8a26a0a9..c8f620f6 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -165,6 +165,7 @@ 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 From cea86adb39e36acfab4fcfab9f40b7d97d53dcfa Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Fri, 22 Dec 2017 15:51:00 +1300 Subject: [PATCH 23/57] Fix method call in Asset.calls property --- bitshares/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bitshares/asset.py b/bitshares/asset.py index 41c3183d..15efe069 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -128,7 +128,7 @@ def feed(self): @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 From 8096f75d4dfd2b7bb5b0bf80e2cd2da0243ec654 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Fri, 22 Dec 2017 16:09:50 +1300 Subject: [PATCH 24/57] Add basic test for Asset.calls property --- requirements-test.txt | 1 + tests/test_asset.py | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 tests/test_asset.py diff --git a/requirements-test.txt b/requirements-test.txt index 0e485d2a..ee2fa0f2 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -4,4 +4,5 @@ scrypt==0.7.1 Events==0.2.2 pyyaml pytest +pytest-mock coverage diff --git a/tests/test_asset.py b/tests/test_asset.py new file mode 100644 index 00000000..6f9b95f6 --- /dev/null +++ b/tests/test_asset.py @@ -0,0 +1,9 @@ +from bitshares.asset import Asset +from bitshares import BitShares + + +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) From 84d7cd9ce042d1f9185d11b1e8d5ba8c4d2700a1 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Fri, 22 Dec 2017 16:53:32 +1300 Subject: [PATCH 25/57] Complete fix for market symbol split (started in #8) --- bitshares/market.py | 12 ++++-------- bitshares/price.py | 6 +++--- bitshares/utils.py | 10 ++++++++++ tests/test_utils.py | 6 ++++++ 4 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 tests/test_utils.py diff --git a/bitshares/market.py b/bitshares/market.py index 4620ea16..9aeb95f8 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, formatTimeString +from .utils import ( + formatTimeFromNow, formatTime, formatTimeString, assets_from_string) from .asset import Asset from .amount import Amount from .price import Price, Order, FilledOrder @@ -40,7 +41,6 @@ class Market(dict): quote** and obtain/pay **only base**. """ - market_sep_regex = "[/\-:]" def __init__( self, @@ -53,7 +53,7 @@ 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}) @@ -62,10 +62,6 @@ def __init__( 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`` @@ -75,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 diff --git a/bitshares/price.py b/bitshares/price.py index a7280813..d544aea0 100644 --- a/bitshares/price.py +++ b/bitshares/price.py @@ -5,7 +5,7 @@ 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): @@ -76,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"]) @@ -148,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"]) diff --git a/bitshares/utils.py b/bitshares/utils.py index 0546a4a0..efb63220 100644 --- a/bitshares/utils.py +++ b/bitshares/utils.py @@ -1,3 +1,4 @@ +import re import time from datetime import datetime @@ -35,3 +36,12 @@ def parse_time(block_time): """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) 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'] From efab4b1327cb2f38d44166bd9805e0ebd3e93af8 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Thu, 28 Dec 2017 12:12:00 +0100 Subject: [PATCH 26/57] [feeds] allow to specify a different cer when not having BTS as collateral --- bitshares/bitshares.py | 26 ++++++++++++++++---------- bitshares/market.py | 2 ++ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index a87eb593..cd88518a 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -1217,6 +1217,7 @@ def publish_price_feed( 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"] @@ -1224,31 +1225,36 @@ 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), }, diff --git a/bitshares/market.py b/bitshares/market.py index c96539f1..37aa8270 100644 --- a/bitshares/market.py +++ b/bitshares/market.py @@ -57,6 +57,8 @@ def __init__( 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)) From f75ff79e3e0034840f24fc36c06e74db6c19f94b Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Thu, 28 Dec 2017 13:17:37 +0100 Subject: [PATCH 27/57] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3cc4ea46..44398b06 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.9' setup( name='bitshares', From 08376c91d3526a0a714e482b31e04d465f50e195 Mon Sep 17 00:00:00 2001 From: BroncoTc Date: Tue, 2 Jan 2018 14:08:28 +0800 Subject: [PATCH 28/57] fix apt command error --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9e9ee349..5b62257a 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Visit the [pybitshares website](http://docs.pybitshares.com/en/latest/) for in d ### Install with pip: ``` -$ sudo apt-get install libffi-dev libssl-dev python-dev python-dev3 +$ sudo apt-get install libffi-dev libssl-dev python-dev python3-dev python3-pip $ pip3 install bitshares ``` @@ -24,4 +24,4 @@ $ python3 setup.py install --user ### Upgrade ``` $ pip3 install --user --upgrade -``` \ No newline at end of file +``` From d4332e0486ea063b5b445d4f3bb4b8ba597867c5 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Thu, 4 Jan 2018 08:19:00 +0100 Subject: [PATCH 29/57] [block] Block Header for easier access to time etc. --- bitshares/block.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/bitshares/block.py b/bitshares/block.py index 4a3af34a..7187b1f2 100644 --- a/bitshares/block.py +++ b/bitshares/block.py @@ -39,3 +39,19 @@ 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) + + def time(self): + """ Return a datatime instance for the timestamp of this block + """ + return parse_time(self['timestamp']) From 85f46cd65a816066eb23a24ae37093991e503ffc Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 5 Jan 2018 14:19:41 +0100 Subject: [PATCH 30/57] [message] initial message signing support --- bitshares/__init__.py | 18 +++++++ bitshares/bitshares.py | 104 ++++++++++++++++++++++++++++++++++++++++ bitshares/exceptions.py | 12 +++++ bitshares/wallet.py | 7 ++- tests/test_bitshares.py | 18 ++++++- 5 files changed, 156 insertions(+), 3 deletions(-) diff --git a/bitshares/__init__.py b/bitshares/__init__.py index 5fa70f71..0c390950 100644 --- a/bitshares/__init__.py +++ b/bitshares/__init__.py @@ -18,3 +18,21 @@ "vesting", "proposal" ] + +SIGNED_MESSAGE_META = """{message} +account={meta[account]} +memokey={meta[memokey]} +block={meta[block]} +timestamp={meta[timestamp]}""" + +SIGNED_MESSAGE_ENCAPSULATED = """ +-----BEGIN BITSHARES SIGNED MESSAGE----- +{message} +-----BEGIN META----- +account={meta[account]} +memokey={meta[memokey]} +block={meta[block]} +timestamp={meta[timestamp]} +-----BEGIN SIGNATURE----- +{signature} +-----END BITSHARES SIGNED MESSAGE-----""" diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index cd88518a..08f8fa63 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -22,6 +22,7 @@ AccountDoesNotExistsException, InsufficientAuthorityError, MissingKeyError, + InvalidMessageSignature, ) from .wallet import Wallet from .transactionbuilder import TransactionBuilder, ProposalBuilder @@ -1397,3 +1398,106 @@ def fund_fee_pool(self, symbol, amount, account=None, **kwargs): "extensions": [] }) return self.finalizeOp(op, account, "active", **kwargs) + + def sign_message(self, message, account=None, **kwargs): + """ Sign a message with an account's memo key + + :param str message: Message to sign + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + + :returns: the signed message encapsulated in a known format + """ + from graphenebase.ecdsa import sign_message + from binascii import hexlify + from . import ( + SIGNED_MESSAGE_META, + SIGNED_MESSAGE_ENCAPSULATED + ) + 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) + info = self.info() + message = message.strip() + meta = dict( + timestamp=info["time"], + block=info["head_block_number"], + memokey=account["options"]["memo_key"], + account=account["name"]) + + # wif key + wif = self.wallet.getPrivateKeyForPublicKey( + account["options"]["memo_key"] + ) + print(SIGNED_MESSAGE_META.format(**locals())) + + # signature + signature = hexlify(sign_message( + SIGNED_MESSAGE_META.format(**locals()), wif + )).decode("ascii") + + return SIGNED_MESSAGE_ENCAPSULATED.format(**locals()) + + def verify_message(self, message, **kwargs): + """ Verify a message with an account's memo key + + :param str message: Ecapsulated Message to verify + :param str account: (optional) the account that owns the bet + (defaults to ``default_account``) + + :returns: the signed message encapsulated in a known format + """ + from graphenebase.ecdsa import verify_message + from binascii import hexlify, unhexlify + from . import ( + SIGNED_MESSAGE_META, + SIGNED_MESSAGE_ENCAPSULATED + ) + # Split message into its parts + obj = re.split( + ( + "-----BEGIN BITSHARES SIGNED MESSAGE-----|" + "-----BEGIN META-----|" + "-----BEGIN SIGNATURE-----|" + "-----END BITSHARES SIGNED MESSAGE-----" + ), + message) + parts = [o.strip() for o in obj] + assert len(parts) == 5 + + message = parts[1] + signature = parts[3] + # Parse the meta data + meta = dict(re.findall(r'(\S+)=(.*)', parts[2])) + + # 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) + + # 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()) + print(message) + + pubkey = verify_message(message, unhexlify(signature)) + pk = PublicKey(hexlify(pubkey).decode("ascii")) + if format(pk, self.rpc.chain_params["prefix"]) != meta["memokey"]: + raise InvalidMessageSignature diff --git a/bitshares/exceptions.py b/bitshares/exceptions.py index 0443b118..f620bbac 100644 --- a/bitshares/exceptions.py +++ b/bitshares/exceptions.py @@ -100,3 +100,15 @@ 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/wallet.py b/bitshares/wallet.py index c8f620f6..2237967d 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -4,6 +4,7 @@ from bitsharesbase.account import PrivateKey, GPHPrivateKey from .account import Account from .exceptions import ( + KeyNotFound, InvalidWifError, WalletExists, WrongMasterPasswordException, @@ -223,8 +224,10 @@ def getPrivateKeyForPublicKey(self, pub): if not self.created(): raise NoWalletException - return self.decrypt_wif( - self.keyStorage.getPrivateKeyForPublicKey(pub)) + 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 diff --git a/tests/test_bitshares.py b/tests/test_bitshares.py index dcdf5374..24110cbf 100644 --- a/tests/test_bitshares.py +++ b/tests/test_bitshares.py @@ -1,3 +1,4 @@ +import mock import string import unittest import random @@ -20,7 +21,7 @@ def __init__(self, *args, **kwargs): self.bts = BitShares( "wss://node.testnet.bitshares.eu", nobroadcast=True, - keys={"active": wif, "owner": wif}, + keys={"active": wif, "owner": wif, "memo": wif}, ) # from getpass import getpass # self.bts.wallet.unlock(getpass()) @@ -225,3 +226,18 @@ def test_approvecommittee(self): self.assertIn( "0:11", op["new_options"]["votes"]) + + def test_sign_message(self): + def new_refresh(self): + dict.__init__( + self, {"name": "init0", + "options": { + "memo_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + }}) + + with mock.patch( + "bitshares.account.Account.refresh", + new=new_refresh + ): + p = self.bts.sign_message("message foobar") + self.bts.verify_message(p) From e46c35723f691e9891480efa22d969edfa7e3dd9 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 5 Jan 2018 14:26:53 +0100 Subject: [PATCH 31/57] [txbuilder] do not raise exception if key not found --- bitshares/transactionbuilder.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bitshares/transactionbuilder.py b/bitshares/transactionbuilder.py index 17f1a6f7..e49e9c28 100644 --- a/bitshares/transactionbuilder.py +++ b/bitshares/transactionbuilder.py @@ -209,10 +209,12 @@ def fetchkeys(account, perm, level=0): 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 From ce470e32bba575b8be6a37505c342c933d14fcc0 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Mon, 8 Jan 2018 13:20:23 +0100 Subject: [PATCH 32/57] [message] setup separate class for message signing --- bitshares/__init__.py | 21 +----- bitshares/bitshares.py | 160 ++++++++--------------------------------- bitshares/message.py | 135 ++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 149 deletions(-) create mode 100644 bitshares/message.py diff --git a/bitshares/__init__.py b/bitshares/__init__.py index 0c390950..915ef88d 100644 --- a/bitshares/__init__.py +++ b/bitshares/__init__.py @@ -16,23 +16,6 @@ "wallet", "committee", "vesting", - "proposal" + "proposal", + "message" ] - -SIGNED_MESSAGE_META = """{message} -account={meta[account]} -memokey={meta[memokey]} -block={meta[block]} -timestamp={meta[timestamp]}""" - -SIGNED_MESSAGE_ENCAPSULATED = """ ------BEGIN BITSHARES SIGNED MESSAGE----- -{message} ------BEGIN META----- -account={meta[account]} -memokey={meta[memokey]} -block={meta[block]} -timestamp={meta[timestamp]} ------BEGIN SIGNATURE----- -{signature} ------END BITSHARES SIGNED MESSAGE-----""" diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index 08f8fa63..3a0fcf45 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -22,7 +22,6 @@ AccountDoesNotExistsException, InsufficientAuthorityError, MissingKeyError, - InvalidMessageSignature, ) from .wallet import Wallet from .transactionbuilder import TransactionBuilder, ProposalBuilder @@ -180,6 +179,10 @@ def connect(self, self.rpc = BitSharesNodeRPC(node, rpcuser, rpcpassword, **kwargs) + @property + def prefix(self): + return self.rpc.chain_params["prefix"] + def newWallet(self, pwd): """ Create a new wallet. This method is basically only calls :func:`bitshares.wallet.create`. @@ -441,7 +444,7 @@ def transfer(self, to, amount, asset, memo="", account=None, **kwargs): "asset_id": amount.asset["id"] }, "memo": memoObj.encrypt(memo), - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account, "active", **kwargs) @@ -549,18 +552,18 @@ def create_account( 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"]) + active_key, prefix=self.prefix) owner_pubkey = PublicKey( - owner_key, prefix=self.rpc.chain_params["prefix"]) + owner_key, prefix=self.prefix) memo_pubkey = PublicKey( - memo_key, prefix=self.rpc.chain_params["prefix"]) + 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]] @@ -606,7 +609,7 @@ 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", **kwargs) @@ -627,7 +630,7 @@ def upgrade_account(self, account=None, **kwargs): "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", **kwargs) @@ -685,7 +688,7 @@ def allow( 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 @@ -710,7 +713,7 @@ def allow( "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", **kwargs) @@ -746,7 +749,7 @@ def disallow( 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"])) @@ -816,7 +819,7 @@ def update_memo_key(self, key, account=None, **kwargs): 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 @@ -864,7 +867,7 @@ def approvewitness(self, witnesses, account=None, **kwargs): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active", **kwargs) @@ -902,7 +905,7 @@ def disapprovewitness(self, witnesses, account=None, **kwargs): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active", **kwargs) @@ -939,7 +942,7 @@ def approvecommittee(self, committees, account=None, **kwargs): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active", **kwargs) @@ -977,7 +980,7 @@ def disapprovecommittee(self, committees, account=None, **kwargs): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active", **kwargs) @@ -997,7 +1000,7 @@ def approveproposal( 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"] + is_key = approver and approver[:3] == self.prefix if not approver and not is_key: approver = account elif approver and not is_key: @@ -1016,7 +1019,7 @@ def approveproposal( 'fee_paying_account': account["id"], 'proposal': proposal["id"], 'active_approvals_to_add': [approver["id"]], - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix } if is_key: update_dict.update({ @@ -1063,7 +1066,7 @@ def disapproveproposal( 'fee_paying_account': account["id"], 'proposal': proposal["id"], 'active_approvals_to_remove': [approver["id"]], - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix })) return self.finalizeOp(op, account["name"], "active", **kwargs) @@ -1095,7 +1098,7 @@ def approveworker(self, workers, account=None, **kwargs): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active", **kwargs) @@ -1128,7 +1131,7 @@ def disapproveworker(self, workers, account=None, **kwargs): "account": account["id"], "new_options": options, "extensions": {}, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active", **kwargs) @@ -1157,7 +1160,7 @@ def cancel(self, orderNumbers, account=None, **kwargs): "fee_paying_account": account["id"], "order": order, "extensions": [], - "prefix": self.rpc.chain_params["prefix"]})) + "prefix": self.prefix})) return self.finalizeOp(op, account["name"], "active", **kwargs) def vesting_balance_withdraw(self, vesting_id, amount=None, account=None, **kwargs): @@ -1188,7 +1191,7 @@ def vesting_balance_withdraw(self, vesting_id, amount=None, account=None, **kwar "amount": int(amount), "asset_id": amount["asset"]["id"] }, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active") @@ -1259,7 +1262,7 @@ def publish_price_feed( "maximum_short_squeeze_ratio": int(mssr * 10), "maintenance_collateral_ratio": int(mcr * 10), }, - "prefix": self.rpc.chain_params["prefix"] + "prefix": self.prefix }) return self.finalizeOp(op, account["name"], "active") @@ -1274,7 +1277,7 @@ def update_witness(self, witness_identifier, url=None, key=None, **kwargs): 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, @@ -1398,106 +1401,3 @@ def fund_fee_pool(self, symbol, amount, account=None, **kwargs): "extensions": [] }) return self.finalizeOp(op, account, "active", **kwargs) - - def sign_message(self, message, account=None, **kwargs): - """ Sign a message with an account's memo key - - :param str message: Message to sign - :param str account: (optional) the account that owns the bet - (defaults to ``default_account``) - - :returns: the signed message encapsulated in a known format - """ - from graphenebase.ecdsa import sign_message - from binascii import hexlify - from . import ( - SIGNED_MESSAGE_META, - SIGNED_MESSAGE_ENCAPSULATED - ) - 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) - info = self.info() - message = message.strip() - meta = dict( - timestamp=info["time"], - block=info["head_block_number"], - memokey=account["options"]["memo_key"], - account=account["name"]) - - # wif key - wif = self.wallet.getPrivateKeyForPublicKey( - account["options"]["memo_key"] - ) - print(SIGNED_MESSAGE_META.format(**locals())) - - # signature - signature = hexlify(sign_message( - SIGNED_MESSAGE_META.format(**locals()), wif - )).decode("ascii") - - return SIGNED_MESSAGE_ENCAPSULATED.format(**locals()) - - def verify_message(self, message, **kwargs): - """ Verify a message with an account's memo key - - :param str message: Ecapsulated Message to verify - :param str account: (optional) the account that owns the bet - (defaults to ``default_account``) - - :returns: the signed message encapsulated in a known format - """ - from graphenebase.ecdsa import verify_message - from binascii import hexlify, unhexlify - from . import ( - SIGNED_MESSAGE_META, - SIGNED_MESSAGE_ENCAPSULATED - ) - # Split message into its parts - obj = re.split( - ( - "-----BEGIN BITSHARES SIGNED MESSAGE-----|" - "-----BEGIN META-----|" - "-----BEGIN SIGNATURE-----|" - "-----END BITSHARES SIGNED MESSAGE-----" - ), - message) - parts = [o.strip() for o in obj] - assert len(parts) == 5 - - message = parts[1] - signature = parts[3] - # Parse the meta data - meta = dict(re.findall(r'(\S+)=(.*)', parts[2])) - - # 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) - - # 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()) - print(message) - - pubkey = verify_message(message, unhexlify(signature)) - pk = PublicKey(hexlify(pubkey).decode("ascii")) - if format(pk, self.rpc.chain_params["prefix"]) != meta["memokey"]: - raise InvalidMessageSignature diff --git a/bitshares/message.py b/bitshares/message.py new file mode 100644 index 00000000..1668631b --- /dev/null +++ b/bitshares/message.py @@ -0,0 +1,135 @@ +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__) + +SIGNED_MESSAGE_META = """{message} +account={meta[account]} +memokey={meta[memokey]} +block={meta[block]} +timestamp={meta[timestamp]}""" + +SIGNED_MESSAGE_ENCAPSULATED = """ +-----BEGIN BITSHARES SIGNED MESSAGE----- +{message} +-----BEGIN META----- +account={meta[account]} +memokey={meta[memokey]} +block={meta[block]} +timestamp={meta[timestamp]} +-----BEGIN SIGNATURE----- +{signature} +-----END BITSHARES SIGNED MESSAGE-----""" + +MESSAGE_SPLIT = ( + "-----BEGIN BITSHARES SIGNED MESSAGE-----\\n|" + "\\n-----BEGIN META-----|" + "-----BEGIN SIGNATURE-----|" + "-----END BITSHARES SIGNED MESSAGE-----" +) + + +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) + 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 + signature = hexlify(sign_message( + SIGNED_MESSAGE_META.format( + message=self.message, + **locals(), + ), + wif + )).decode("ascii") + + return SIGNED_MESSAGE_ENCAPSULATED.format( + message=self.message, + **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(MESSAGE_SPLIT, self.message) + assert len(parts) == 5 + + message = parts[1] + signature = parts[3].rstrip().strip() + # Parse the meta data + meta = dict(re.findall(r'(\S+)=(.*)', parts[2])) + + # 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) + + # 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 From 77243625b014ff94d9d47826670b104d489c9f93 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Mon, 8 Jan 2018 13:30:02 +0100 Subject: [PATCH 33/57] [message] minor fixes to self calls --- bitshares/message.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/bitshares/message.py b/bitshares/message.py index 1668631b..612764c1 100644 --- a/bitshares/message.py +++ b/bitshares/message.py @@ -58,7 +58,7 @@ def sign(self, account=None, **kwargs): raise ValueError("You need to provide an account") # Data for message - account = Account(account, bitshares_instance=self) + account = Account(account, bitshares_instance=self.bitshares) info = self.bitshares.info() meta = dict( timestamp=info["time"], @@ -72,17 +72,14 @@ def sign(self, account=None, **kwargs): ) # signature + message = self.message signature = hexlify(sign_message( - SIGNED_MESSAGE_META.format( - message=self.message, - **locals(), - ), + SIGNED_MESSAGE_META.format(**locals()), wif )).decode("ascii") - return SIGNED_MESSAGE_ENCAPSULATED.format( - message=self.message, - **locals()) + message = self.message + return SIGNED_MESSAGE_ENCAPSULATED.format(**locals()) def verify(self, **kwargs): """ Verify a message with an account's memo key @@ -109,7 +106,7 @@ def verify(self, **kwargs): assert "timestamp" in meta # Load account from blockchain - account = Account(meta.get("account"), bitshares_instance=self) + 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"]: From 344f27ab515a96c4d38cf1686bf14eaf57dbedcd Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Mon, 8 Jan 2018 16:38:23 +0100 Subject: [PATCH 34/57] minor cleanup --- bitshares/asset.py | 1 + bitshares/bitshares.py | 6 ------ tests/test_bitshares.py | 4 ++-- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/bitshares/asset.py b/bitshares/asset.py index b6b8ea3e..ef3f1f57 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -75,6 +75,7 @@ def is_fully_loaded(self): "bitasset_data_id" in self and "bitasset_data" in self ) + @property def symbol(self): return self["symbol"] diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index 3a0fcf45..da884883 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,9 +16,6 @@ from .storage import configStorage as config from .exceptions import ( AccountExistsException, - AccountDoesNotExistsException, - InsufficientAuthorityError, - MissingKeyError, ) from .wallet import Wallet from .transactionbuilder import TransactionBuilder, ProposalBuilder diff --git a/tests/test_bitshares.py b/tests/test_bitshares.py index 24110cbf..0cb6af59 100644 --- a/tests/test_bitshares.py +++ b/tests/test_bitshares.py @@ -239,5 +239,5 @@ def new_refresh(self): "bitshares.account.Account.refresh", new=new_refresh ): - p = self.bts.sign_message("message foobar") - self.bts.verify_message(p) + p = Message("message foobar").sign() + Message(p).verify() From d501936ac092f155fe90043bfbaf4e8e7817edde Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Wed, 10 Jan 2018 08:38:16 +0100 Subject: [PATCH 35/57] [setup] dependency of graphenelib pushed by one release --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 44398b06..0c3594e1 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'Topic :: Office/Business :: Financial', ], install_requires=[ - "graphenelib>=0.5.5", + "graphenelib>=0.5.6", "websockets", "appdirs", "Events", From d8dcc7b5cfc334a6c00405004a308e39d8e209b6 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 12 Jan 2018 15:45:19 +0100 Subject: [PATCH 36/57] Reserve from proper account --- bitshares/exceptions.py | 6 ++++++ bitshares/transactionbuilder.py | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/bitshares/exceptions.py b/bitshares/exceptions.py index f620bbac..7583b0bd 100644 --- a/bitshares/exceptions.py +++ b/bitshares/exceptions.py @@ -5,6 +5,12 @@ class WalletExists(Exception): pass +class WalletLocked(Exception): + """ Wallet is locked + """ + pass + + class AccountExistsException(Exception): """ The requested account already exists """ diff --git a/bitshares/transactionbuilder.py b/bitshares/transactionbuilder.py index e49e9c28..7763862d 100644 --- a/bitshares/transactionbuilder.py +++ b/bitshares/transactionbuilder.py @@ -6,7 +6,8 @@ from .exceptions import ( InsufficientAuthorityError, MissingKeyError, - InvalidWifError + InvalidWifError, + WalletLocked ) from bitshares.instance import shared_bitshares_instance import logging @@ -204,6 +205,9 @@ def appendSigner(self, account, 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 [] From abada9ffcd2c9845df40fc0515fd1e2395a7283b Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 15:29:05 +0100 Subject: [PATCH 37/57] [wallet] locked() returns False if there are keys preloaded --- bitshares/wallet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bitshares/wallet.py b/bitshares/wallet.py index 2237967d..41c8fbf3 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -130,6 +130,8 @@ def lock(self): def locked(self): """ Is the wallet database locked? """ + if Wallet.keys: # Keys have been manually provided! + return False try: self.tryUnlockFromEnv() except: From 7d590c0ea5cfb2874085f8558db70af04eaf8dbb Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 15:49:25 +0100 Subject: [PATCH 38/57] Fix #36 --- bitshares/asset.py | 29 +++++++++++++++++++------- bitshares/block.py | 7 +++++-- bitshares/blockchain.py | 46 ++++++++++++++++++++++++++++------------- bitshares/committee.py | 9 +++++--- bitshares/dex.py | 44 ++++++++++++++++++++++++++++----------- bitshares/market.py | 10 +++++++-- bitshares/notify.py | 27 ++++++++++++++++++------ bitshares/price.py | 42 ++++++++++++++++++++++++++++--------- bitshares/proposal.py | 4 ++-- bitshares/vesting.py | 10 +++++---- bitshares/wallet.py | 4 ++-- bitshares/witness.py | 4 ++-- bitshares/worker.py | 7 ++++--- 13 files changed, 174 insertions(+), 69 deletions(-) diff --git a/bitshares/asset.py b/bitshares/asset.py index ef3f1f57..95ffc781 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -40,7 +40,7 @@ def __init__( asset, lazy=lazy, full=full, - bitshares_instance=None + bitshares_instance=bitshares_instance ) def refresh(self): @@ -49,7 +49,7 @@ def refresh(self): asset = self.bitshares.rpc.get_asset(self.identifier) if not asset: raise AssetDoesNotExistsException(self.identifier) - super(Asset, self).__init__(asset) + 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( @@ -115,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 @@ -123,7 +126,10 @@ 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): @@ -137,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"], @@ -202,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, } diff --git a/bitshares/block.py b/bitshares/block.py index 7187b1f2..99b5edfe 100644 --- a/bitshares/block.py +++ b/bitshares/block.py @@ -33,7 +33,7 @@ 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 @@ -49,7 +49,10 @@ def refresh(self): block = self.bitshares.rpc.get_block_header(self.identifier) if not block: raise BlockDoesNotExistsException - super(BlockHeader, self).__init__(block) + 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 a867e771..ab3593f8 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``. @@ -96,7 +106,8 @@ def blocks(self, start=None, stop=None): :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! + * "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") @@ -126,13 +137,15 @@ def blocks(self, start=None, stop=None): 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! + * "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 @@ -158,7 +171,8 @@ def stream(self, opNames=[], *args, **kwargs): :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! + * "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 +190,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 @@ -194,10 +209,13 @@ def awaitTxConfirmation(self, transaction, limit=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/committee.py b/bitshares/committee.py index 9d0cece7..c615619c 100644 --- a/bitshares/committee.py +++ b/bitshares/committee.py @@ -18,7 +18,8 @@ def refresh(self): if self.test_valid_objectid(self.identifier): _, i, _ = self.identifier.split(".") if int(i) == 2: - account = Account(self.identifier) + account = Account( + self.identifier, bitshares_instance=self.bitshares) member = self.bitshares.rpc.get_committee_member_by_account( account["id"]) elif int(i) == 5: @@ -27,13 +28,15 @@ def refresh(self): raise CommitteeMemberDoesNotExistsException else: # maybe identifier is an account name - account = Account(self.identifier) + 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) + super(Committee, self).__init__( + member, bitshares_instance=self.bitshares) self.account_id = account["id"] @property diff --git a/bitshares/dex.py b/bitshares/dex.py index 87d5777c..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,16 +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 @@ -160,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"] @@ -177,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/market.py b/bitshares/market.py index 37aa8270..f57e92c8 100644 --- a/bitshares/market.py +++ b/bitshares/market.py @@ -536,7 +536,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): @@ -548,5 +551,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/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 8161c45f..f1ed9b4a 100644 --- a/bitshares/price.py +++ b/bitshares/price.py @@ -99,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 @@ -120,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) @@ -247,12 +247,24 @@ def __mul__(self, other): # 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"]) - a["quote"] = Amount(float(self["quote"]) * float(other["quote"]), other["quote"]["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"]) - a["quote"] = Amount(float(self["quote"]) * float(other["quote"]), self["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): @@ -285,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"] @@ -558,7 +576,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 5ad88605..07471dc7 100644 --- a/bitshares/proposal.py +++ b/bitshares/proposal.py @@ -19,7 +19,7 @@ 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): @@ -35,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/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 41c8fbf3..7db5c8ad 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -325,7 +325,7 @@ def getAllAccounts(self, pub): """ for id in self.getAccountsFromPublicKey(pub): try: - account = Account(id) + account = Account(id) # FIXME: self.bitshares is not available in wallet! except: continue yield {"name": account["name"], @@ -342,7 +342,7 @@ def getAccount(self, 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 return {"name": account["name"], diff --git a/bitshares/witness.py b/bitshares/witness.py index dff4ca44..675f30e5 100644 --- a/bitshares/witness.py +++ b/bitshares/witness.py @@ -28,11 +28,11 @@ def refresh(self): 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): diff --git a/bitshares/worker.py b/bitshares/worker.py index 15ef8ddb..a1b2498d 100644 --- a/bitshares/worker.py +++ b/bitshares/worker.py @@ -21,12 +21,13 @@ 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): @@ -39,7 +40,7 @@ class Workers(list): def __init__(self, account_name=None, bitshares_instance=None): self.bitshares = bitshares_instance or shared_bitshares_instance() if account_name: - account = Account(account_name) + account = Account(account_name, bitshares_instance=self.bitshares) self.workers = self.bitshares.rpc.get_workers_by_account( account["id"]) else: From 0a85d8a8c895b6b5776f553934308cf09243d235 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 15:52:53 +0100 Subject: [PATCH 39/57] close #33 --- bitshares/market.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bitshares/market.py b/bitshares/market.py index f57e92c8..ff8ea646 100644 --- a/bitshares/market.py +++ b/bitshares/market.py @@ -518,6 +518,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 From c526fe7307dbdebb5b6d1e8cdc47efe8f5cbaaa2 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 15:59:46 +0100 Subject: [PATCH 40/57] close #18 --- bitshares/account.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bitshares/account.py b/bitshares/account.py index a646f7d1..44fef3d3 100644 --- a/bitshares/account.py +++ b/bitshares/account.py @@ -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,19 @@ def openorders(self): """ Returns open Orders """ from .price import Order - if not self.full: + self.ensure_full() + return [Order(o) 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, From 8c3a5486b00487d212dd7cd4a9b30fdfca93fef4 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 16:05:33 +0100 Subject: [PATCH 41/57] close #27 --- bitshares/price.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bitshares/price.py b/bitshares/price.py index f1ed9b4a..1451bf85 100644 --- a/bitshares/price.py +++ b/bitshares/price.py @@ -493,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 From c4e86a78bc250c29950549f00ec79264539e75a1 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 16:21:27 +0100 Subject: [PATCH 42/57] Fix #11 --- bitshares/account.py | 2 +- bitshares/memo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bitshares/account.py b/bitshares/account.py index 44fef3d3..0bd7edf9 100644 --- a/bitshares/account.py +++ b/bitshares/account.py @@ -47,7 +47,7 @@ def __init__( account, lazy=lazy, full=full, - bitshares_instance=None + bitshares_instance=bitshares_instance ) def refresh(self): diff --git a/bitshares/memo.py b/bitshares/memo.py index b33e6bdd..c70d39d6 100644 --- a/bitshares/memo.py +++ b/bitshares/memo.py @@ -63,7 +63,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 From fa9b1f42503b4b19d268d1785415415e430f9ae7 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 16:27:02 +0100 Subject: [PATCH 43/57] fix #21 --- bitshares/account.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bitshares/account.py b/bitshares/account.py index 0bd7edf9..a5b85d3b 100644 --- a/bitshares/account.py +++ b/bitshares/account.py @@ -129,7 +129,10 @@ def openorders(self): """ from .price import Order self.ensure_full() - return [Order(o) for o in self["limit_orders"]] + return [ + Order(o, bitshares_instance=self.bitshares) + for o in self["limit_orders"] + ] @property def is_fully_loaded(self): From 7edc45217e9decd3f3ff2c42da1d7e2ca41dc443 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 16:47:46 +0100 Subject: [PATCH 44/57] fix test --- tests/test_bitshares.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_bitshares.py b/tests/test_bitshares.py index 0cb6af59..cb3a13f6 100644 --- a/tests/test_bitshares.py +++ b/tests/test_bitshares.py @@ -8,6 +8,7 @@ from bitshares.amount import Amount from bitsharesbase.account import PrivateKey from bitshares.instance import set_shared_bitshares_instance +from bitshares.message import Message wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" core_unit = "TEST" From 6922bed45440abafb04f9b8941d569f3183af276 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 16:48:12 +0100 Subject: [PATCH 45/57] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 0c3594e1..9b38df95 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.9' +VERSION = '0.1.10' setup( name='bitshares', From 5f0f80687214a30f061eaff160589cda01c1d360 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Fri, 19 Jan 2018 16:49:56 +0100 Subject: [PATCH 46/57] cleanup makefile --- Makefile | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 From 118a55fa2378e916aa4abeb135c501adc1eee221 Mon Sep 17 00:00:00 2001 From: Chris Beaven Date: Sat, 20 Jan 2018 23:32:56 +1300 Subject: [PATCH 47/57] Comment about the magic `mocker` argument --- tests/test_asset.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_asset.py b/tests/test_asset.py index 6f9b95f6..5c67dad8 100644 --- a/tests/test_asset.py +++ b/tests/test_asset.py @@ -2,6 +2,8 @@ from bitshares import BitShares +# 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') From 00549ced672eba9845ce33b65f0726872ca3c7fa Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Mon, 22 Jan 2018 12:17:07 +0100 Subject: [PATCH 48/57] [operations] asset_* operations --- bitsharesbase/objects.py | 44 ++++---- bitsharesbase/operations.py | 66 +++++++++++- tests/test_transactions.py | 198 +++++++++++++++++++++++++++++------- 3 files changed, 249 insertions(+), 59 deletions(-) diff --git a/bitsharesbase/objects.py b/bitsharesbase/objects.py index 89b9fba0..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 @@ -195,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 32a77bef..696dd276 100644 --- a/bitsharesbase/operations.py +++ b/bitsharesbase/operations.py @@ -18,6 +18,7 @@ PriceFeed, Permission, AccountOptions, + BitAssetOptions, AssetOptions, ObjectId, Worker_initializer, @@ -80,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: @@ -102,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/tests/test_transactions.py b/tests/test_transactions.py index dedfb4d4..df88a680 100644 --- a/tests/test_transactions.py +++ b/tests/test_transactions.py @@ -350,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, @@ -493,6 +457,166 @@ 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": [] + }) + self.cm = ("f68585abf4dce7c80457010e000000000000000000000000000" + "00000000000000102c0ded2bc1f1305fb0faac5e6c03ee3a192" + "4234985427b6167ca569d13df435cf02c0ded2bc1f1305fb0fa" + "ac5e6c03ee3a1924234985427b6167ca569d13df435cf8c94d1" + "9817945c5120fa5b6e83079a878e499e2e52a76a7739e9de409" + "86a8e3bd8a68ce316cee50b210000012055139900ea2ae7db9d" + "4ef0d5d4015d2d993d0590ad32662bda94daba74a5e13411aef" + "4de6f847e9e4300e5c8c36aa8e5f9032d25fd8ca01abd58c7e9" + "528677e4") + self.doit() + def compareConstructedTX(self): self.maxDiff = None self.op = operations.Bid_collateral(**{ @@ -515,7 +639,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()) From 3137f8b1f269c023bf86fb074c641a2cadba3232 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Mon, 22 Jan 2018 14:23:59 +0100 Subject: [PATCH 49/57] bump dependency on python-graphene --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9b38df95..153fcf70 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'Topic :: Office/Business :: Financial', ], install_requires=[ - "graphenelib>=0.5.6", + "graphenelib>=0.5.7", "websockets", "appdirs", "Events", From 03707966849e8ba32645fbbd67c0e76b89a1587d Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 23 Jan 2018 09:57:51 +0100 Subject: [PATCH 50/57] [memo] simplify the use of bitshares.memo --- bitshares/memo.py | 54 ++++++++++++++++++++++++++++++++------------- bitshares/wallet.py | 9 ++++++++ docs/memo.rst | 45 +++++++++++++++++-------------------- 3 files changed, 68 insertions(+), 40 deletions(-) diff --git a/bitshares/memo.py b/bitshares/memo.py index c70d39d6..2a198b66 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,38 @@ 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 encrypt(self, memo): """ Encrypt a memo @@ -86,19 +100,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.bitshares.rpc.chain_params["prefix"] - ), + PublicKey(pubkey, prefix=self.bitshares.prefix), memo.get("nonce"), memo.get("message") ) diff --git a/bitshares/wallet.py b/bitshares/wallet.py index 7db5c8ad..cf847e11 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -7,6 +7,7 @@ KeyNotFound, InvalidWifError, WalletExists, + WalletLocked, WrongMasterPasswordException, NoWalletException ) @@ -127,6 +128,11 @@ 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? """ @@ -226,6 +232,9 @@ def getPrivateKeyForPublicKey(self, pub): if not self.created(): raise NoWalletException + if not self.unlocked(): + raise WalletLocked + encwif = self.keyStorage.getPrivateKeyForPublicKey(pub) if not encwif: raise KeyNotFound("No private key for {} found".format(pub)) diff --git a/docs/memo.rst b/docs/memo.rst index 1f1283a1..980602bc 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,29 @@ 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 -########### + block = Block(23755086) + transaction = block["transactions"][3] + op = transaction["operations"][0] + op_id = op[0] + op_data = op[1] + + memo = Memo() + memo.bitshares.wallet.unlock(getpass()) + print(memo.decrypt(op_data["memo"])) + +API +### .. automodule:: bitsharesbase.memo :members: From 4008070db8b0df62d9715fca371dbfca8e660897 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 23 Jan 2018 10:19:55 +0100 Subject: [PATCH 51/57] [memo] improve interface of bitshares.memo --- bitshares/bitshares.py | 28 ++++++++++++++++++---------- bitshares/exceptions.py | 6 ++++++ bitshares/memo.py | 6 ++++++ bitshares/wallet.py | 10 ++++++---- docs/memo.rst | 21 ++++++++++++++------- 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/bitshares/bitshares.py b/bitshares/bitshares.py index da884883..b10737d2 100644 --- a/bitshares/bitshares.py +++ b/bitshares/bitshares.py @@ -177,16 +177,6 @@ def connect(self, def prefix(self): return self.rpc.chain_params["prefix"] - 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 - """ - self.wallet.create(pwd) - def set_default_account(self, account): """ Set the default account to be used """ @@ -300,6 +290,24 @@ 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 # ------------------------------------------------------------------------- diff --git a/bitshares/exceptions.py b/bitshares/exceptions.py index 7583b0bd..d8206507 100644 --- a/bitshares/exceptions.py +++ b/bitshares/exceptions.py @@ -11,6 +11,12 @@ class WalletLocked(Exception): pass +class RPCConnectionRequired(Exception): + """ An RPC connection is required + """ + pass + + class AccountExistsException(Exception): """ The requested account already exists """ diff --git a/bitshares/memo.py b/bitshares/memo.py index 2a198b66..d7ec659a 100644 --- a/bitshares/memo.py +++ b/bitshares/memo.py @@ -56,6 +56,12 @@ def __init__( 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 diff --git a/bitshares/wallet.py b/bitshares/wallet.py index cf847e11..2e2b5fc9 100644 --- a/bitshares/wallet.py +++ b/bitshares/wallet.py @@ -9,7 +9,8 @@ WalletExists, WalletLocked, WrongMasterPasswordException, - NoWalletException + NoWalletException, + RPCConnectionRequired ) log = logging.getLogger(__name__) @@ -54,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: diff --git a/docs/memo.rst b/docs/memo.rst index 980602bc..c4992887 100644 --- a/docs/memo.rst +++ b/docs/memo.rst @@ -76,14 +76,21 @@ Decoding of a received memo from bitshares.block import Block from bitshares.memo import Memo - block = Block(23755086) - transaction = block["transactions"][3] - op = transaction["operations"][0] - op_id = op[0] - op_data = op[1] - + # 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() - memo.bitshares.wallet.unlock(getpass()) + + # 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 From 24903b5b3a9315420f88d8862ec366ff4eee31c3 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 23 Jan 2018 10:57:54 +0100 Subject: [PATCH 52/57] [tests] fix test case for asset_issue --- tests/test_transactions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_transactions.py b/tests/test_transactions.py index df88a680..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" @@ -604,7 +604,8 @@ def test_asset_issue(self): "message": encrypted_memo, }, "issue_to_account": "1.2.0", - "extensions": [] + "extensions": [], + "prefix": prefix }) self.cm = ("f68585abf4dce7c80457010e000000000000000000000000000" "00000000000000102c0ded2bc1f1305fb0faac5e6c03ee3a192" From b46cd1486ba6b04626354a55d8588b6f8a2d4b96 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 23 Jan 2018 12:09:07 +0100 Subject: [PATCH 53/57] [docs] add worker.rst --- docs/index.rst | 1 + 1 file changed, 1 insertion(+) 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 From 223b27481f73a83581e9bec4dab9127df62e7347 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 23 Jan 2018 12:56:49 +0100 Subject: [PATCH 54/57] [docs] fix intendation errors when building docs --- bitshares/asset.py | 5 +++-- bitshares/blockchain.py | 15 ++++++--------- bitshares/witness.py | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/bitshares/asset.py b/bitshares/asset.py index 95ffc781..adc11129 100644 --- a/bitshares/asset.py +++ b/bitshares/asset.py @@ -497,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} @@ -520,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/blockchain.py b/bitshares/blockchain.py index ab3593f8..a6614216 100644 --- a/bitshares/blockchain.py +++ b/bitshares/blockchain.py @@ -105,9 +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") @@ -143,9 +142,8 @@ def ops(self, start=None, stop=None, **kwargs): :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 @@ -170,9 +168,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 diff --git a/bitshares/witness.py b/bitshares/witness.py index 675f30e5..bd437935 100644 --- a/bitshares/witness.py +++ b/bitshares/witness.py @@ -9,7 +9,7 @@ class Witness(BlockchainObject): :param str account_name: Name of the witness :param bitshares bitshares_instance: BitShares() instance to use when - accesing a RPC + accesing a RPC """ type_ids = [6, 2] @@ -39,7 +39,7 @@ 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 + accesing a RPC """ def __init__(self, bitshares_instance=None): self.bitshares = bitshares_instance or shared_bitshares_instance() From b16d82fc931b194d237c9efe611f7ba2ebed590a Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 23 Jan 2018 14:49:02 +0100 Subject: [PATCH 55/57] [blockchain] fix 'stop' parameter in blocks() --- bitshares/blockchain.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/bitshares/blockchain.py b/bitshares/blockchain.py index a6614216..61eff3a3 100644 --- a/bitshares/blockchain.py +++ b/bitshares/blockchain.py @@ -118,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): @@ -130,7 +133,8 @@ 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) From a1cd2ef2cb32609979d4d42f26a9a6f65c4d3f84 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Tue, 23 Jan 2018 15:53:34 +0100 Subject: [PATCH 56/57] bump dependency version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 153fcf70..a6dc898b 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ 'Topic :: Office/Business :: Financial', ], install_requires=[ - "graphenelib>=0.5.7", + "graphenelib>=0.5.8", "websockets", "appdirs", "Events", From 26fa540ce406b5ceca9c9da4e180438e9b6f69f2 Mon Sep 17 00:00:00 2001 From: Fabian Schuh Date: Wed, 24 Jan 2018 12:45:24 +0100 Subject: [PATCH 57/57] [message] improvments to message parsing --- bitshares/message.py | 46 ++++++++++++++----------- tests/test_bitshares.py | 16 --------- tests/test_message.py | 75 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 tests/test_message.py diff --git a/bitshares/message.py b/bitshares/message.py index 612764c1..21481a1f 100644 --- a/bitshares/message.py +++ b/bitshares/message.py @@ -11,6 +11,13 @@ 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]} @@ -18,23 +25,17 @@ timestamp={meta[timestamp]}""" SIGNED_MESSAGE_ENCAPSULATED = """ ------BEGIN BITSHARES SIGNED MESSAGE----- +{MESSAGE_SPLIT[0]} {message} ------BEGIN META----- +{MESSAGE_SPLIT[1]} account={meta[account]} memokey={meta[memokey]} block={meta[block]} timestamp={meta[timestamp]} ------BEGIN SIGNATURE----- +{MESSAGE_SPLIT[2]} {signature} ------END BITSHARES SIGNED MESSAGE-----""" - -MESSAGE_SPLIT = ( - "-----BEGIN BITSHARES SIGNED MESSAGE-----\\n|" - "\\n-----BEGIN META-----|" - "-----BEGIN SIGNATURE-----|" - "-----END BITSHARES SIGNED MESSAGE-----" -) +{MESSAGE_SPLIT[3]} +""" class Message(): @@ -72,14 +73,17 @@ def sign(self, account=None, **kwargs): ) # signature - message = self.message + message = self.message.strip() signature = hexlify(sign_message( SIGNED_MESSAGE_META.format(**locals()), wif )).decode("ascii") message = self.message - return SIGNED_MESSAGE_ENCAPSULATED.format(**locals()) + return SIGNED_MESSAGE_ENCAPSULATED.format( + MESSAGE_SPLIT=MESSAGE_SPLIT, + **locals() + ) def verify(self, **kwargs): """ Verify a message with an account's memo key @@ -91,13 +95,15 @@ def verify(self, **kwargs): :raises InvalidMessageSignature if the signature is not ok """ # Split message into its parts - parts = re.split(MESSAGE_SPLIT, self.message) - assert len(parts) == 5 + 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[1] - signature = parts[3].rstrip().strip() + message = parts[0].strip() + signature = parts[2].strip() # Parse the meta data - meta = dict(re.findall(r'(\S+)=(.*)', parts[2])) + meta = dict(re.findall(r'(\S+)=(.*)', parts[1])) # Ensure we have all the data in meta assert "account" in meta @@ -106,7 +112,9 @@ def verify(self, **kwargs): assert "timestamp" in meta # Load account from blockchain - account = Account(meta.get("account"), bitshares_instance=self.bitshares) + 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"]: diff --git a/tests/test_bitshares.py b/tests/test_bitshares.py index cb3a13f6..864de94d 100644 --- a/tests/test_bitshares.py +++ b/tests/test_bitshares.py @@ -8,7 +8,6 @@ from bitshares.amount import Amount from bitsharesbase.account import PrivateKey from bitshares.instance import set_shared_bitshares_instance -from bitshares.message import Message wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" core_unit = "TEST" @@ -227,18 +226,3 @@ def test_approvecommittee(self): self.assertIn( "0:11", op["new_options"]["votes"]) - - def test_sign_message(self): - def new_refresh(self): - dict.__init__( - self, {"name": "init0", - "options": { - "memo_key": "TEST6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" - }}) - - with mock.patch( - "bitshares.account.Account.refresh", - new=new_refresh - ): - p = Message("message foobar").sign() - Message(p).verify() 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()