diff --git a/docs/index.md b/docs/index.md index 5718d19d1..20b22d1c4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,34 +1,284 @@ -# Introduction +## Introduction -PyTezos library is a Python toolset for Tezos blockchain, +PyTezos library is a Python toolset for Tezos blockchain, including work with keys, signatures, contracts, operations, +RPC query builder, and a high-level interface for smart contract interaction. It can be used to build a full-fledged +application, but also it's perfect for doing researches in Jupyter interactive notebooks. +In this quick start guide, we'll go through the main concepts and inspect one of the common use cases. -# Setting up +## Installation -Ubuntu +First of all you'll probably need to install cryptographic libraries in your system. + +Ubuntu: ``` $ sudo apt install libsodium-dev libsecp256k1-dev libgmp-dev ``` -MacOS +MacOS: ``` $ brew install libsodium ``` - +After that just install PyTezos from PyPi: ``` $ pip install pytezos ``` -# PyTezos client +That's it! You can open Python console or Jupyter notebook and get to the next step. +## Set key and RPC node + +All active interaction with the blockchain starts with the PyTezosClient: ```python >>> from pytezos import pytezos >>> pytezos + + +Key +tz1grSQDByRpnVs7sPtaprNZRp531ZKz6Jmm + +Node +https://tezos-dev.cryptonomic-infra.tech/ (alphanet) + +Helpers +.Contract() +.Key() +.Proto() +.account() +.activate_account() +.alphanet() +.ballot() +.contract() +.delegation() +.double_baking_evidence() +.double_endorsement_evidence() +.endorsement() +.mainnet() +.operation() +.operation_group() +.origination() +.proposals() +.reveal() +.seed_nonce_revelation() +.transaction() +.using() +.zeronet() +``` + +This is one of the cool features in the interactive mode: aside from the autocomplete and call docstrings, +you can see the list of available methods for class, or list of arguments and return value for a particular methods. +We are interested in `using` method, which is responsible for setting up manager key and RPC connection. +```python +>>> pytezos.using + + +Change current rpc endpoint and account (private key) +:param shell: one of 'alphanet', 'mainnet', 'zeronet', RPC node uri, or instance of `ShellQuery` +:param key: base58 encoded key, path to the faucet file, alias from tezos-client, or instance of `Key` +:return: A copy of current object with changes applied +``` + +Note, that by default `pytezos` is initialized with `alphanet` and a predefined private key for demo purpose, +so you can start to interact immediately, but it's highly recommended to use your own key. Let's do that! + +Go to the [https://faucet.tzalpha.net/](https://faucet.tzalpha.net/) and download key file. +Then configure the client (we can leave `shell` parameter empty, but we will set it explicitly for better understanding) +```python +>>> pytezos = pytezos.using( +... key='~/Downloads/tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa.json', +... shell='https://rpc.tulip.tools/alphanet/') +``` + +Public available RPC providers are available at `pytezos.rpc`: +```python +>>> from pytezos.rpc import tulip +>>> tulip + + +Networks +.mainnet # https://rpc.tulip.tools/mainnet/ +.alphanet # https://rpc.tulip.tools/alphanet/ +.zeronet # https://rpc.tulip.tools/zeronet/ +``` + +## Activate account + +In order to start using our faucet account we need to claim balance. +```python +>>> pytezos.activate_account + + +Activate recommended allocations for contributions to the TF fundraiser. +More info https://activate.tezos.com/ +:param pkh: Public key hash, leave empty for autocomplete +:param activation_code: Secret code from pdf, leave empty for autocomplete +:return: dict or OperationGroup +``` + +Cool! We can just leave all fields empty and let PyTezos do all the work. There are two autocomplete function available: +`fill` and `autofill`. The only difference is that `autofill` simulates the operation and sets precise values for fee +and gas/storage limits. +```python +>>> pytezos.activate_account().autofill() + + +Key +tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa + +Node +https://rpc.tulip.tools/alphanet/ (alphanet) + +Payload +{'branch': 'BL5UtKR4ysFLwcK2ign1h2KoZLJY88zd1vzWUZPzto9iEJqUj1d', + 'contents': [{'kind': 'activate_account', + 'pkh': 'tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa', + 'secret': 'e8d47034af5ea23a9613dba219f8b4a792b22c5f'}], + 'protocol': 'Pt24m4xiPbLDhVgVfABUjirbmda3yohdN82Sp9FeuAXJ4eV9otd', + 'signature': None} + +Helpers +.activate_account() +.autofill() +.ballot() +.binary_payload() +.delegation() +.double_baking_evidence() +.double_endorsement_evidence() +.endorsement() +.fill() +.forge() +.hash() +.inject() +.json_payload() +.operation() +.origination() +.preapply() +.proposals() +.reveal() +.run() +.seed_nonce_revelation() +.sign() +.transaction() +.using() +``` +Have you noticed that operation helpers are still available? We can easily chain operations but we cannot for example +put `activate_account` and `reveal` together because they are from different validation passes. +Ok, let's sign and preapply operation to see what's going to happen: +```python +>>> pytezos.activate_account().fill().sign().preapply() +[{'contents': [{'kind': 'activate_account', + 'pkh': 'tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa', + 'secret': 'e8d47034af5ea23a9613dba219f8b4a792b22c5f', + 'metadata': {'balance_updates': [{'kind': 'contract', + 'contract': 'tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa', + 'change': '10848740286'}]}}], + 'signature': 'sigRg96wY6mxLKJ7jaTrVcXzABqhyEa4J1Ji5rGPKPVHv2YugViGfeH1b7qu7eavhhEGoASqffwjnH2fr46oBXVZrMWC6ZFg'}] +``` + +Everything looks good! Ready to inject the operation. +```python +>>> pytezos.activate_account().fill().sign().inject() +'oo77zoEsa9RuA7NubhvckM8NBNta8dUbL4e5GuhXmqnZ9XQGK5k' +``` + +We can search our operation in the node mempool to check what status it has: +```python +>>> pytezos.shell.mempool.pending_operations['oo77zoEsa9RuA7NubhvckM8NBNta8dUbL4e5GuhXmqnZ9XQGK5k'] +{'status': 'applied', + 'hash': 'oo77zoEsa9RuA7NubhvckM8NBNta8dUbL4e5GuhXmqnZ9XQGK5k', + 'branch': 'BMdgSQxnGGTXuvGp7qrgnM2pMu16dS9Hdjq9UbdHGxzxfKfVR75', + 'contents': [{'kind': 'activate_account', + 'pkh': 'tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa', + 'secret': 'e8d47034af5ea23a9613dba219f8b4a792b22c5f'}], + 'signature': 'sigbMMUu6h9vAxoM7ZZdwttDk2CcgpwbmCrFjSTQBtTsoLFYNdz85wCKQBMZ2ZMEVrBnt61XGZXyWuuDDbp7WepjHgR6DTrT'} + +>>> pytezos.account() +{'manager': 'tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa', + 'balance': '10848740286', + 'spendable': True, + 'delegate': {'setable': False}, + 'counter': '715917'} ``` -# Sending operations +Yay! We have claimed our account balance. + +## Reveal public key + +```python +>>> pytezos.reveal().autofill().sign().inject() +'oo3TzPdNhtz5nmE9nL2yGLqwUzSfmb1vjTpu8wFkX5CTKLV67AE' +``` + +We can also search for operation by hash if we know exact block level or that it was injected recently: +```python +>>> pytezos.shell.blocks[580244].operations['oo3TzPdNhtz5nmE9nL2yGLqwUzSfmb1vjTpu8wFkX5CTKLV67AE'] + + +Path +/chains/main/blocks/580244/operations/3/16 + +() +The `m-th` operation in the `n-th` validation pass of the block. +:return: Object + +Helpers +.unsigned() + +>>> pytezos.shell.blocks[-20:].find_operation('oo3TzPdNhtz5nmE9nL2yGLqwUzSfmb1vjTpu8wFkX5CTKLV67AE') +{'protocol': 'Pt24m4xiPbLDhVgVfABUjirbmda3yohdN82Sp9FeuAXJ4eV9otd', + 'chain_id': 'NetXgtSLGNJvNye', + 'hash': 'oo3TzPdNhtz5nmE9nL2yGLqwUzSfmb1vjTpu8wFkX5CTKLV67AE', + 'branch': 'BLdKQLeV5FaPspBLP6J7Tx5xs2XRRH7pJGnXhwEW1uKz9PGBF8H', + 'contents': [{'kind': 'reveal', + 'source': 'tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa', + 'fee': '1261', + 'counter': '715918', + 'gas_limit': '10000', + 'storage_limit': '0', + 'public_key': 'edpktn9Xg5TaBJ9j6gs1X4AAsQR43zxzmaVNdyerq2PxTy7dUfN3X8', + 'metadata': {'balance_updates': [{'kind': 'contract', + 'contract': 'tz1cnQZXoznhduu4MVWfJF6GSyP6mMHMbbWa', + 'change': '-1261'}, + {'kind': 'freezer', + 'category': 'fees', + 'delegate': 'tz3gN8NTLNLJg5KRsUU47NHNVHbdhcFXjjaB', + 'cycle': 283, + 'change': '1261'}], + 'operation_result': {'status': 'applied', 'consumed_gas': '10000'}}}], + 'signature': 'sigjzUVPWuFKxmMizHfMUgjqXpo2cqNEHjgRDykqwWiot2129KRWCanZjytUfxFWSDwpNSjkakmWqzhxLwNNcBcQQWJ5mAsW'} +``` + +## Originate contract + +Now we can do something interesting. Let's deploy a Michelson smart contract! First we need to load data, in this +tutorial we will get it from Michelson source file. There are plenty of available methods, but we'are interested in +`script` which gives us payload for origination. +```python +>>> from pytezos import Contract +>>> contract = Contract.from_file('~/Documents/demo_contract.tz') +>>> contract.script + + +Generate script for contract origination +:param storage: Python object, leave None to generate empty +:return: {"code": $Micheline, "storage": $Micheline} +``` + +PyTezos can generate empty storage based on the type description. This is a small part of the high-level interface +functionality we will further learn. + +```python +>>> pytezos.origination(script=contract.script()).autofill().sign().inject() +'op3ZRdR6LjmA8AeNEjKEimr2uQeAWwXSXEftUBiTVx4k86Rw66m' + +>>> opg = pytezos.shell.blocks[-5:].find_operation('op3ZRdR6LjmA8AeNEjKEimr2uQeAWwXSXEftUBiTVx4k86Rw66m') +>>> contract_id = opg['contents'][0]['metadata']['operation_result']['originated_contracts'][0] +>>> contract_id +'KT1RX74ty3TqBfU6pBs7ce3uV7PLBrUEav6X' +``` + +## Interact with contract + -# Interacting with contracts -# Querying data diff --git a/mkdocs.yml b/mkdocs.yml index a838a5f7e..bc7aa5795 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: PyTezos nav: - Quick start: index.md - - User guide: guide.md + - Developer guide: guide.md theme: readthedocs diff --git a/pyproject.toml b/pyproject.toml index 562cc86e0..2521fa429 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,17 @@ [tool.poetry] name = "pytezos" -version = "1.2.2" -description = "Python utils for Tezos" -authors = ["Michael Zaikin ", "Arthur Breitman", "Roman Serikov"] +version = "2.0.0" +description = "Python toolkit for Tezos" license = "MIT" +authors = ["Michael Zaikin ", "Arthur Breitman", "Roman Serikov"] readme = "README.md" repository = "https://github.com/baking-bad/pytezos" keywords = ['tezos', 'crypto', 'blockchain', 'xtz'] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] [tool.poetry.dependencies] python = "^3.6" diff --git a/pytezos/interop.py b/pytezos/interop.py index 0675d4d3d..05836f991 100644 --- a/pytezos/interop.py +++ b/pytezos/interop.py @@ -1,6 +1,6 @@ from os.path import isfile -from pytezos.rpc import ShellQuery, mainnet, alphanet, zeronet +from pytezos.rpc import ShellQuery, RpcNode, mainnet, alphanet, zeronet from pytezos.crypto import Key from pytezos.encoding import is_key from pytezos.tools.docstring import InlineDocstring @@ -11,6 +11,16 @@ class Interop(metaclass=InlineDocstring): + def __repr__(self): + res = [ + super(Interop, self).__repr__(), + '\nKey', + self.key.public_key_hash(), + '\nNode', + f'{self.shell.node.uri} ({self.shell.node.network})' + ] + return '\n'.join(res) + def __init__(self, shell=None, key=None): if shell is None: shell = default_shell @@ -21,7 +31,10 @@ def __init__(self, shell=None, key=None): 'alphanet': alphanet, 'zeronet': zeronet } - self.shell = networks[shell] + if shell in networks: + self.shell = networks[shell] + else: + self.shell = ShellQuery(node=RpcNode(uri=shell)) elif isinstance(shell, ShellQuery): self.shell = shell else: @@ -48,8 +61,8 @@ def _spawn(self, **kwargs): def using(self, shell: ShellQuery = None, key: Key = None): """ Change current rpc endpoint and account (private key) - :param shell: one of 'alphanet', 'mainnet', 'zeronet', instance of `ShellQuery` - :param key: base58 encoded key or instance of `Key` + :param shell: one of 'alphanet', 'mainnet', 'zeronet', or RPC node uri, or instance of `ShellQuery` + :param key: base58 encoded key, path to the faucet file, alias from tezos-client, or instance of `Key` :return: A copy of current object with changes applied """ return self._spawn(shell=shell, key=key) diff --git a/pytezos/michelson/contract.py b/pytezos/michelson/contract.py index 3647a4d37..38709d0c5 100644 --- a/pytezos/michelson/contract.py +++ b/pytezos/michelson/contract.py @@ -1,5 +1,5 @@ from functools import lru_cache -from os.path import basename +from os.path import basename, exists, expanduser from pytezos.tools.docstring import get_class_docstring, InlineDocstring from pytezos.michelson.coding import build_schema, decode_micheline, encode_micheline, Schema, \ @@ -327,3 +327,32 @@ def from_file(cls, path): """ with open(path) as f: return cls.from_michelson(f.read()) + + def save_file(self, path, overwrite=False): + """ + Save Michelson code to file + :param path: Output path + :param overwrite: Default is False + """ + path = expanduser(path) + if exists(path) and not overwrite: + raise FileExistsError(path) + + with open(path, 'w+') as f: + f.write(self.text) + + def script(self, storage=None): + """ + Generate script for contract origination + :param storage: Python object, leave None to generate empty + :return: {"code": $Micheline, "storage": $Micheline} + """ + if storage is None: + storage = self.storage.default() + else: + storage = self.storage.encode(storage) + + return { + "code": self.code, + "storage": storage + } diff --git a/pytezos/michelson/formatter.py b/pytezos/michelson/formatter.py index 759b594a3..1f25ca2f3 100644 --- a/pytezos/michelson/formatter.py +++ b/pytezos/michelson/formatter.py @@ -30,8 +30,8 @@ def is_script(node): def format_node(node, indent='', inline=False, is_root=False, wrapped=False): if isinstance(node, list): - seq_indent = indent + ' ' * 2 is_script_root = is_root and is_script(node) + seq_indent = indent if is_script_root else indent + ' ' * 2 items = list(map(lambda x: format_node(x, seq_indent, inline, wrapped=True), node)) if items: length = len(indent) + sum(map(len, items)) + 4 diff --git a/pytezos/operation/content.py b/pytezos/operation/content.py index 2fd6ac14b..0a598b060 100644 --- a/pytezos/operation/content.py +++ b/pytezos/operation/content.py @@ -191,12 +191,11 @@ def transaction(self, destination, amount=0, parameters=None, }) @inline_doc - def origination(self, code=None, storage=None, manager_pubkey='', balance=0, delegatable=None, spendable=None, + def origination(self, script=None, manager_pubkey='', balance=0, delegatable=None, spendable=None, source='', counter=0, fee=0, gas_limit=0, storage_limit=0): """ Deploy smart contract (scriptless KT accounts are not used for delegation since Babylon) - :param code: Micheline expression (array of three elements: [{parameter}, {storage}, {code}]) - :param storage: Micheline expression, keep None if you want to use default (autogenerated) + :param script: {"code": $Micheline, "storage": $Micheline}, default is None (until Babylon) :param manager_pubkey: Public key hash of the manager's address, leave None to use signatory address :param balance: Amount transferred on the balance, WARNING: there is no default way to withdraw funds. More info: https://tezos.stackexchange.com/questions/1315/can-i-withdraw-funds-from-an-empty-smart-contract @@ -209,15 +208,10 @@ def origination(self, code=None, storage=None, manager_pubkey='', balance=0, del :param storage_limit: Leave None for autocomplete :return: dict or OperationGroup """ - if code: - script = {'code': code, 'storage': storage} - else: - script = {} - if delegatable is None: - delegatable = code is None + delegatable = script is None if spendable is None: - spendable = code is None + spendable = script is None return self.operation({ 'kind': 'origination', @@ -228,7 +222,7 @@ def origination(self, code=None, storage=None, manager_pubkey='', balance=0, del 'storage_limit': str(storage_limit), 'manager_pubkey': manager_pubkey, 'balance': format_mutez(balance), - 'script': script, + 'script': script or {}, 'spendable': spendable, 'delegatable': delegatable }) diff --git a/pytezos/operation/fees.py b/pytezos/operation/fees.py index 99c69312c..5b8ef3859 100644 --- a/pytezos/operation/fees.py +++ b/pytezos/operation/fees.py @@ -22,6 +22,9 @@ def gas_limit(self, content): def storage_limit(self, content): raise NotImplementedError + def burn_cap(self, content): + raise NotImplementedError + class Athens004FeesProvider(FeesProvider, protocol='Pt24m4xiPbLDhVgVfABUjirbmda3yohdN82Sp9FeuAXJ4eV9otd'): hard_gas_limit_per_operation = 400000 @@ -30,12 +33,12 @@ class Athens004FeesProvider(FeesProvider, protocol='Pt24m4xiPbLDhVgVfABUjirbmda3 minimal_nanotez_per_byte = 1 minimal_nanotez_per_gas_unit = .1 - def calculate_fee(self, content, consumed_gas, extra_size): + def calculate_fee(self, content, consumed_gas, extra_size, reserve=10): size = len(forge_operation(content)) + extra_size fee = self.minimal_fees \ + self.minimal_nanotez_per_byte * size \ + int(self.minimal_nanotez_per_gas_unit * consumed_gas) - return fee + return fee + reserve def fee(self, content): return self.calculate_fee( @@ -48,7 +51,7 @@ def gas_limit(self, content): values = { 'reveal': 10000, 'delegation': 10000, - 'origination': 10000, + 'origination': self.hard_gas_limit_per_operation if content.get('script') else 10000, 'transaction': self.hard_gas_limit_per_operation if content.get('parameters') else 10200 } return values.get(content['kind']) @@ -61,3 +64,12 @@ def storage_limit(self, content): 'transaction': self.hard_storage_limit_per_operation if content.get('parameters') else 257 } return values.get(content['kind']) + + def burn_cap(self, content): + values = { + 'reveal': 0, + 'delegation': 0, + 'origination': 257, + 'transaction': 0 if content.get('parameters') else 257 + } + return values.get(content['kind']) diff --git a/pytezos/operation/group.py b/pytezos/operation/group.py index 5e8b9dca7..3e3307418 100644 --- a/pytezos/operation/group.py +++ b/pytezos/operation/group.py @@ -168,17 +168,20 @@ def res_limits(res): return int(res.get('consumed_gas', 0)), int(res.get('paid_storage_size_diff', 0)) def fill_content(content): - consumed = [res_limits(content['metadata']['operation_result'])] \ - + list(map(res_limits, content['metadata'].get('internal_operation_result', []))) + operation_result = content['metadata'].get('operation_result') + if operation_result: + internal_operation_result = content['metadata'].get('internal_operation_result', []) - consumed_gas, paid_storage_diff = tuple(map(sum, zip(*consumed))) - fee = fees_provider.calculate_fee(content, consumed_gas, extra_size) + consumed = [res_limits(operation_result)] + list(map(res_limits, internal_operation_result)) + consumed_gas, paid_storage_diff = tuple(map(sum, zip(*consumed))) + fee = fees_provider.calculate_fee(content, consumed_gas, extra_size) + + content.update( + gas_limit=str(consumed_gas), + storage_limit=str(paid_storage_diff + fees_provider.burn_cap(content)), + fee=str(fee) + ) - content.update( - gas_limit=str(consumed_gas), - storage_limit=str(paid_storage_diff), - fee=str(fee) - ) content.pop('metadata') return content diff --git a/pytezos/rpc/__init__.py b/pytezos/rpc/__init__.py index 6cae57deb..443794542 100644 --- a/pytezos/rpc/__init__.py +++ b/pytezos/rpc/__init__.py @@ -12,11 +12,19 @@ def __init__(self, **urls): @lru_cache(maxsize=None) def __getattr__(self, network) -> ShellQuery: - return ShellQuery(node=RpcNode(self.urls[network])) + return ShellQuery(node=RpcNode(uri=self.urls[network], network=network)) def __dir__(self): return list(super(RpcProvider, self).__dir__()) + list(self.urls.keys()) + def __repr__(self): + res = [ + super(RpcProvider, self).__repr__(), + '\nNetworks', + *list(map(lambda x: f'.{x[0]} # {x[1]}', self.urls.items())) + ] + return '\n'.join(res) + localhost = RpcProvider( mainnet='https://127.0.0.1:8732/', diff --git a/pytezos/rpc/node.py b/pytezos/rpc/node.py index 9bcb3dd2c..6af8175ed 100644 --- a/pytezos/rpc/node.py +++ b/pytezos/rpc/node.py @@ -20,18 +20,26 @@ def __str__(self): class RpcNode: - def __init__(self, uri): - self._uri = uri + def __init__(self, uri, network=''): + self.uri = uri + self.network = network self._cache = dict() self._session = requests.Session() def __repr__(self): - return f'Node address\n{self._uri}\n\nCached items\n' + '\n'.join(self._cache.keys()) + res = [ + super(RpcNode, self).__repr__(), + '\nNode address', + f'{self.uri} ({self.network})', + '\nCached urls', + *list(self._cache.keys()) + ] + return '\n'.join(res) def request(self, method, path, **kwargs) -> requests.Response: res = self._session.request( method=method, - url=urljoin(self._uri, path), + url=urljoin(self.uri, path), headers={ 'content-type': 'application/json', 'user-agent': 'PyTezos' diff --git a/pytezos/rpc/protocol.py b/pytezos/rpc/protocol.py index b75ff36d2..9840ea562 100644 --- a/pytezos/rpc/protocol.py +++ b/pytezos/rpc/protocol.py @@ -57,7 +57,7 @@ def __getitem__(self, block_id): return BlockSliceQuery( start=block_id.start, stop=block_id.stop, - node=self._node, + node=self.node, path=self._path, params=self._params ) @@ -76,7 +76,7 @@ def voting_period(self): return BlockSliceQuery( start=-metadata['level']['voting_period_position'], head='head', - node=self._node, + node=self.node, path=self._path, params=self._params ) @@ -90,7 +90,7 @@ def cycle(self): return BlockSliceQuery( start=-metadata['level']['cycle_position'], head='head', - node=self._node, + node=self.node, path=self._path, params=self._params ) @@ -334,7 +334,7 @@ def __getitem__(self, proposal_id) -> ProposalQuery: return ProposalQuery( path=self._path + '/{}', params=self._params + [proposal_id], - node=self._node + node=self.node ) def __repr__(self): diff --git a/pytezos/rpc/query.py b/pytezos/rpc/query.py index 1a8492bbc..852f2c574 100644 --- a/pytezos/rpc/query.py +++ b/pytezos/rpc/query.py @@ -64,7 +64,7 @@ def __init_subclass__(cls, path='', **kwargs): cls.__extensions__[path] = cls def __init__(self, node: RpcNode, path='', caching=False, params=None, timeout=None): - self._node = node + self.node = node self._path = path self._caching = caching self._timeout = timeout @@ -84,7 +84,7 @@ def _spawn_query(self, path, params): child_class = self.__extensions__.get(path, RpcQuery) return child_class( path=path, - node=self._node, + node=self.node, caching=self._caching, params=params ) @@ -94,7 +94,7 @@ def _query_path(self): return self._path.format(*self._params) def __call__(self, **params): - return self._node.get( + return self.node.get( path=self._query_path, params=params, caching=self._caching @@ -121,7 +121,7 @@ def __getitem__(self, child_id): ) def _get(self, params=None): - return self._node.get( + return self.node.get( path=self._query_path, params=params, caching=self._caching, @@ -129,7 +129,7 @@ def _get(self, params=None): ) def _post(self, json=None, params=None): - return self._node.post( + return self.node.post( path=self._query_path, params=params, json=json, @@ -137,13 +137,13 @@ def _post(self, json=None, params=None): ) def _put(self, params=None): - return self._node.put( + return self.node.put( path=self._query_path, params=params ) def _delete(self, params=None): - return self._node.delete( + return self.node.delete( path=self._query_path, params=params ) diff --git a/pytezos/rpc/search.py b/pytezos/rpc/search.py index 037077467..f868395c4 100644 --- a/pytezos/rpc/search.py +++ b/pytezos/rpc/search.py @@ -158,6 +158,21 @@ def get_counter(x): ) return self[level].operations.origination(contract_id) + def find_operation(self, operation_group_hash): + """ + Find operation by hash + :param operation_group_hash: base58 + :return: dict + """ + last, head = self.get_range() + for block_level in range(head, max(1, last - 1), -1): + try: + return self[block_level].operations[operation_group_hash]() + except StopIteration: + continue + + raise StopIteration(operation_group_hash) + class CyclesQuery(RpcQuery): @@ -198,7 +213,7 @@ def get_range(cycle): return BlockSliceQuery( start=start, stop=stop, - node=self._node, + node=self.node, path=self._path, params=self._params ) diff --git a/pytezos/rpc/shell.py b/pytezos/rpc/shell.py index b86e78d3b..62db651e1 100644 --- a/pytezos/rpc/shell.py +++ b/pytezos/rpc/shell.py @@ -33,7 +33,7 @@ def cycles(self): Operate on cycles rather than blocks. """ return CyclesQuery( - node=self._node, + node=self.node, path=self._path + '/chains/{}/blocks', params=self._params + ['main'] ) @@ -88,6 +88,13 @@ def __getitem__(self, item): return {'status': status, **operation} raise StopIteration + def __repr__(self): + res = [ + super(PendingOperationsQuery, self).__repr__(), + '[]' + get_attr_docstring(self.__class__, '__getitem__') + ] + return '\n'.join(res) + class DescribeQuery(RpcQuery, path='/describe'): @@ -210,7 +217,7 @@ class MonitorQuery(RpcQuery, path=['/monitor/active_chains', '/monitor/valid_blocks']): def __call__(self, *args, **kwargs): - return ResponseGenerator(self._node.request( + return ResponseGenerator(self.node.request( method='GET', path=self._query_path, params=kwargs, @@ -241,7 +248,7 @@ class NetworkLogQuery(RpcQuery, path=['/network/peers/{}/log', '/network/points/ def __call__(self, monitor=False): if monitor: - return ResponseGenerator(self._node.request( + return ResponseGenerator(self.node.request( method='GET', path=self._query_path, stream=True diff --git a/pytezos/tools/docstring.py b/pytezos/tools/docstring.py index 449c2752f..4f1496776 100644 --- a/pytezos/tools/docstring.py +++ b/pytezos/tools/docstring.py @@ -4,6 +4,14 @@ from functools import update_wrapper +def is_interactive(): + import __main__ as main + return not hasattr(main, '__file__') + + +__interactive_mode__ = is_interactive() + + def get_attr_docstring(class_type, attr_name): if attr_name == 'get': attr_name = '__call__' @@ -41,6 +49,9 @@ def attr_format(x): def inline_doc(method): + if not __interactive_mode__: + return method + doc = [repr(method)] if method.__doc__: doc.append(re.sub(r' {3,}', '', method.__doc__))