├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── README.md ├── examples ├── adnl │ ├── adnl.py │ ├── dht.py │ └── dns.py ├── blocks │ ├── block_scanner.py │ └── block_transactios.py ├── extra_currency.py ├── get_methods.py ├── transactions.py └── wallets │ └── highload.py ├── pytoniq ├── __init__.py ├── adnl │ ├── __init__.py │ ├── adnl.py │ ├── dht.py │ └── overlay.py ├── contract │ ├── __init__.py │ ├── contract.py │ ├── nft │ │ ├── __init__.py │ │ ├── nft.py │ │ └── nft_sale.py │ ├── utils.py │ └── wallets │ │ ├── __init__.py │ │ ├── highload.py │ │ ├── highload_v3.py │ │ └── wallet.py └── liteclient │ ├── __init__.py │ ├── _balancer_codegen.py │ ├── balancer.py │ ├── client.py │ ├── sync.py │ └── utils.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py ├── test_adnl.py ├── test_balancer.py ├── test_dht.py ├── test_liteclient.py └── test_wallet.py /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 pytest==7.4.2 pytest-asyncio pytvm 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 37 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pytest 41 | deploy: 42 | if: (github.repository == 'yungwine/pytoniq') && (github.event_name == 'push') 43 | needs: build 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | - name: Set up Python 49 | uses: actions/setup-python@v3 50 | with: 51 | python-version: '3.10' 52 | - name: Install dependencies 53 | run: | 54 | python -m pip install --upgrade pip 55 | pip install build 56 | - name: Build package 57 | run: python -m build 58 | - name: Publish package 59 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 60 | with: 61 | user: __token__ 62 | password: ${{ secrets.PYPI_API_TOKEN }} 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | /test/primitives/mytonlib_test.py 162 | /test/primitives/usr/ 163 | .idea 164 | /feats/contracts/wallets/priv_key.py 165 | /pytoniq/tlb/code_generator.py 166 | /main.py 167 | /venv39 168 | /venv_new 169 | /feats 170 | .DS_Store 171 | /examples/.blockstore/ 172 | /.blockstore/ 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytoniq 2 | 3 | [![PyPI version](https://badge.fury.io/py/pytoniq.svg)](https://badge.fury.io/py/pytoniq) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytoniq)](https://pypi.org/project/pytoniq/) 5 | [![Downloads](https://static.pepy.tech/badge/pytoniq)](https://pepy.tech/project/pytoniq) 6 | [![Downloads](https://static.pepy.tech/badge/pytoniq/month)](https://pepy.tech/project/pytoniq) 7 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 8 | 9 | Pytoniq is a Python SDK for the TON Blockchain. This library extends [pytoniq-core](https://github.com/yungwine/pytoniq-core) with native `LiteClient` and `ADNL`. 10 | 11 | If you have any questions join Python - TON [developers chat](https://t.me/pythonnton). 12 | 13 | ## Documentation 14 | [GitBook](https://yungwine.gitbook.io/pytoniq-doc/) 15 | 16 | ## Installation 17 | 18 | ```commandline 19 | pip install pytoniq 20 | ``` 21 | 22 | ## Examples 23 | You can find them in the [examples](examples/) folder. 24 | 25 | ## LiteClient 26 | 27 | ### General LiteClient usage examples 28 | 29 | #### Client initializing 30 | 31 | ```python 32 | from pytoniq import LiteClient 33 | 34 | 35 | async def main(): 36 | client = LiteClient.from_mainnet_config( # choose mainnet, testnet or custom config dict 37 | ls_i=0, # index of liteserver from config 38 | trust_level=2, # trust level to liteserver 39 | timeout=15 # timeout not includes key blocks synchronization as it works in pytonlib 40 | ) 41 | 42 | await client.connect() 43 | 44 | await client.get_masterchain_info() 45 | 46 | await client.reconnect() # can reconnect to an exising object if had any errors 47 | 48 | await client.close() 49 | 50 | """ or use it with context manager: """ 51 | async with LiteClient.from_mainnet_config(ls_i=0, trust_level=2, timeout=15) as client: 52 | await client.get_masterchain_info() 53 | 54 | ``` 55 | 56 | #### Blocks transactions scanning 57 | 58 | See `BlockScanner` code [here](examples/blocks/block_scanner.py). 59 | 60 | ```python 61 | from pytoniq_core import BlockIdExt 62 | from pytoniq import LiteClient 63 | from examples.blocks.block_scanner import BlockScanner # this import is not available if downloaded from pypi 64 | 65 | async def handle_block(block: BlockIdExt): 66 | if block.workchain == -1: # skip masterchain blocks 67 | return 68 | print(block) 69 | transactions = await client.raw_get_block_transactions_ext(block) 70 | for transaction in transactions: 71 | print(transaction.in_msg) 72 | 73 | 74 | client = LiteClient.from_mainnet_config(ls_i=14, trust_level=0, timeout=20) 75 | 76 | 77 | async def main(): 78 | 79 | await client.connect() 80 | await BlockScanner(client=client, block_handler=handle_block).run() 81 | ``` 82 | 83 | ## LiteBalancer 84 | 85 | `LiteBalancer` is constantly pinging LiteServers to identify "alive" peers. 86 | When you make a request through `LiteBalancer`, it forwards the request to the "best" peer - 87 | the "alive" peer with the maximum last masterchain block seqno among all and minimum average response time. 88 | 89 | `LiteBalancer` can also retry the request if a `asyncio.TimeoutError` occurs, but this must be explicitly set using the 90 | `LiteBalancer.set_max_retries(retries_num)` method. 91 | 92 | ```python 93 | client = LiteBalancer.from_mainnet_config(trust_level=1) 94 | 95 | await client.start_up() 96 | 97 | result = await client.run_get_method(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', method='seqno', stack=[]) 98 | 99 | await client.close_all() 100 | 101 | """ or use it with context manager: """ 102 | 103 | async with LiteBalancer.from_mainnet_config(trust_level=1) as client: 104 | result = await client.run_get_method(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', method='seqno', stack=[]) 105 | 106 | ``` 107 | 108 | Moreover, one of the most important features of `LiteBalancer` is that it detects [archival](https://docs.ton.org/participate/run-nodes/archive-node#overview) LiteServers, 109 | so you can do requests only to archival LiteServers providing `True` for argument `only_archive` in **any** method: 110 | 111 | ```python 112 | # ask for very very old block 113 | blk, _ = await client.lookup_block(-1, -2**63, 100, only_archive=True) 114 | 115 | # ask for old block and run get method for that block: 116 | blk, _ = await client.lookup_block(-1, -2**63, 25000000, only_archive=True) 117 | result = await client.run_get_method(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', method='seqno', 118 | stack=[], block=blk, only_archive=True) 119 | 120 | ``` 121 | 122 | 123 | ### Blockstore 124 | The library can prove all data it receives from a Liteserver (Learn about trust levels [here](https://yungwine.gitbook.io/pytoniq-doc/liteclient/trust-levels)). 125 | If you want to use `LiteClient` or `LiteBalancer` with the zero trust level, at the first time run library will prove block link from the `init_block` to the last masterchain block. 126 | Last proved blocks will be stored in the `.blockstore` folder. The file data contains `ttl` and `gen_utime` of the last synced key block, its data serialized according to the `BlockIdExt` TL scheme (but in big–endian), last synced masterchain block data. 127 | Filename is first 88 bytes of data described above with init block hash. 128 | 129 | ## ADNL 130 | 131 | ```python 132 | from pytoniq.adnl.adnl import AdnlTransport, Node 133 | 134 | adnl = AdnlTransport(timeout=3) 135 | 136 | # start adnl receiving server 137 | await adnl.start() 138 | 139 | # take peer from public config 140 | peer = Node('172.104.59.125', 14432, "/YDNd+IwRUgL0mq21oC0L3RxrS8gTu0nciSPUrhqR78=", adnl) 141 | await adnl.connect_to_peer(peer) 142 | # or await peer.connect() 143 | 144 | await peer.disconnect() 145 | 146 | # send pings 147 | await asyncio.sleep(10) 148 | 149 | # stop adnl receiving server 150 | await adnl.close() 151 | ``` 152 | 153 | ## DHT 154 | 155 | ```python 156 | import time 157 | 158 | from pytoniq.adnl.adnl import AdnlTransport 159 | from pytoniq.adnl.dht import DhtClient, DhtNode 160 | 161 | 162 | adnl = AdnlTransport(timeout=5) 163 | client = DhtClient.from_mainnet_config(adnl) 164 | 165 | await adnl.start() 166 | 167 | foundation_adnl_addr = '516618cf6cbe9004f6883e742c9a2e3ca53ed02e3e36f4cef62a98ee1e449174' 168 | resp = await client.find_value(key=DhtClient.get_dht_key_id(bytes.fromhex(foundation_adnl_addr))) 169 | print(resp) 170 | # {'@type': 'dht.valueFound', 'value': {'key': {'key': {'id': '516618cf6cbe9004f6883e742c9a2e3ca53ed02e3e36f4cef62a98ee1e449174', 'name': b'address', 'idx': 0, '@type': 'dht.key'}, 'id': {'key': '927d3e71e3ce651c3f172134d39163f70e4c792169e39f3d520bfad9388ad4ca', '@type': 'pub.ed25519'}, 'update_rule': {'@type': 'dht.updateRule.signature'}, 'signature': b"g\x08\xf8yo\xed1\xb83\x17\xb9\x10\xb4\x8f\x00\x17]D\xd2\xae\xfa\x87\x9f\xf7\xfa\x192\x971\xee'2\x83\x0fk\x03w\xbb0\xfcU\xc8\x89Zm\x8e\xba\xce \xfc\xde\xf2F\xdb\x0cI*\xe0\xaeN\xef\xc2\x9e\r", '@type': 'dht.keyDescription'}, 'value': {'@type': 'adnl.addressList', 'addrs': [{'@type': 'adnl.address.udp', 'ip': -1537433966, 'port': 3333}], 'version': 1694227845, 'reinit_date': 1694227845, 'priority': 0, 'expire_at': 0}, 'ttl': 1695832194, 'signature': b'z\x8aW\x80k\xceXQ\xff\xb9D{C\x98T\x02e\xef&\xfc\xb6\xde\x80y\xf7\xb4\x92\xae\xd2\xd0\xbakU}3\xfa\xec\x03\xb6v\x98\xb0\xcb\xe8\x05\xb9\xd0\x07o\xb6\xa0)I\x17\xcb\x1a\xc4(Dt\xe6y\x18\x0b', '@type': 'dht.value'}} 171 | 172 | key = client.get_dht_key(id_=adnl.client.get_key_id()) 173 | ts = int(time.time()) 174 | value_data = { 175 | 'addrs': [ 176 | { 177 | "@type": "adnl.address.udp", 178 | "ip": 1111111, 179 | "port": 12000 180 | } 181 | ], 182 | 'version': ts, 183 | 'reinit_date': ts, 184 | 'priority': 0, 185 | 'expire_at': 0, 186 | } 187 | 188 | value = client.schemas.serialize(client.schemas.get_by_name('adnl.addressList'), value_data) 189 | 190 | stored = await client.store_value( # store our address list in dht as value 191 | key=key, 192 | value=value, 193 | private_key=adnl.client.ed25519_private.encode(), 194 | ttl=100, 195 | try_find_after=False 196 | ) 197 | 198 | print(stored) # True if value was stored, False otherwise 199 | 200 | # disconnect from all peers 201 | await client.close() 202 | ``` 203 | -------------------------------------------------------------------------------- /examples/adnl/adnl.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from pytoniq.adnl.adnl import AdnlTransport, Node 5 | 6 | 7 | adnl = AdnlTransport(timeout=3) 8 | 9 | 10 | async def main(): 11 | logging.basicConfig(level=logging.DEBUG) 12 | 13 | # start adnl receiving server 14 | await adnl.start() 15 | 16 | # can set default handler for any query 17 | adnl.set_default_query_handler(handler=lambda i: print(i)) 18 | 19 | # or provide function to process specific queries 20 | def process_get_capabilities_request(_): 21 | return { 22 | '@type': 'tonNode.capabilities', 23 | 'version': 2, 24 | 'capabilities': 1, 25 | } 26 | 27 | adnl.set_query_handler(type_='overlay.getCapabilities', 28 | handler=lambda i: process_get_capabilities_request(i)) 29 | 30 | # take peer from public config 31 | peer = Node('172.104.59.125', 14432, "/YDNd+IwRUgL0mq21oC0L3RxrS8gTu0nciSPUrhqR78=", adnl) 32 | await adnl.connect_to_peer(peer) 33 | 34 | # ask peer for something 35 | await peer.get_signed_address_list() 36 | await adnl.close() 37 | 38 | 39 | # send pings to peer 40 | await asyncio.sleep(2) 41 | 42 | # can disconnect from peer == stop pings 43 | # await peer.disconnect() 44 | 45 | # add another peer 46 | peer = Node('5.161.60.160', 12485, "jXiLaOQz1HPayilWgBWhV9xJhUIqfU95t+KFKQPIpXg=", adnl) 47 | 48 | # second way to connect to peer 49 | await peer.connect() 50 | 51 | # 2 adnl channels 52 | print(adnl.channels) 53 | 54 | # check for pings 55 | await asyncio.sleep(10) 56 | 57 | # stop adnl receiving server 58 | await adnl.close() 59 | 60 | 61 | if __name__ == '__main__': 62 | asyncio.run(main()) 63 | -------------------------------------------------------------------------------- /examples/adnl/dht.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import logging 4 | import time 5 | from pytoniq_core.crypto.ciphers import Server 6 | 7 | from pytoniq.adnl.adnl import AdnlTransport 8 | from pytoniq.adnl.dht import DhtClient, DhtNode 9 | 10 | 11 | adnl = AdnlTransport(timeout=5) 12 | 13 | host = "172.104.59.125" 14 | port = 14432 15 | pub_k = "/YDNd+IwRUgL0mq21oC0L3RxrS8gTu0nciSPUrhqR78=" 16 | peer = DhtNode('172.104.59.125', 14432, "/YDNd+IwRUgL0mq21oC0L3RxrS8gTu0nciSPUrhqR78=", adnl) 17 | 18 | client = DhtClient.from_mainnet_config(adnl) 19 | 20 | # or specify known dht peers explicitly: 21 | # client = DhtClient([peer], adnl) 22 | 23 | 24 | async def main(): 25 | await dht() 26 | await overlay() 27 | 28 | # disconnect from all peers 29 | await client.close() 30 | 31 | 32 | async def dht(): 33 | logging.basicConfig(level=logging.DEBUG) 34 | await adnl.start() 35 | 36 | foundation_adnl_addr = '516618cf6cbe9004f6883e742c9a2e3ca53ed02e3e36f4cef62a98ee1e449174' 37 | resp = await client.find_value(key=DhtClient.get_dht_key_id(bytes.fromhex(foundation_adnl_addr))) 38 | print(resp) 39 | # {'@type': 'dht.valueFound', 'value': {'key': {'key': {'id': '516618cf6cbe9004f6883e742c9a2e3ca53ed02e3e36f4cef62a98ee1e449174', 'name': b'address', 'idx': 0, '@type': 'dht.key'}, 'id': {'key': '927d3e71e3ce651c3f172134d39163f70e4c792169e39f3d520bfad9388ad4ca', '@type': 'pub.ed25519'}, 'update_rule': {'@type': 'dht.updateRule.signature'}, 'signature': b"g\x08\xf8yo\xed1\xb83\x17\xb9\x10\xb4\x8f\x00\x17]D\xd2\xae\xfa\x87\x9f\xf7\xfa\x192\x971\xee'2\x83\x0fk\x03w\xbb0\xfcU\xc8\x89Zm\x8e\xba\xce \xfc\xde\xf2F\xdb\x0cI*\xe0\xaeN\xef\xc2\x9e\r", '@type': 'dht.keyDescription'}, 'value': {'@type': 'adnl.addressList', 'addrs': [{'@type': 'adnl.address.udp', 'ip': -1537433966, 'port': 3333}], 'version': 1694227845, 'reinit_date': 1694227845, 'priority': 0, 'expire_at': 0}, 'ttl': 1695832194, 'signature': b'z\x8aW\x80k\xceXQ\xff\xb9D{C\x98T\x02e\xef&\xfc\xb6\xde\x80y\xf7\xb4\x92\xae\xd2\xd0\xbakU}3\xfa\xec\x03\xb6v\x98\xb0\xcb\xe8\x05\xb9\xd0\x07o\xb6\xa0)I\x17\xcb\x1a\xc4(Dt\xe6y\x18\x0b', '@type': 'dht.value'}} 40 | 41 | key = client.get_dht_key(id_=adnl.client.get_key_id()) 42 | ts = int(time.time()) 43 | value_data = { 44 | 'addrs': [ 45 | { 46 | "@type": "adnl.address.udp", 47 | "ip": 1111111, 48 | "port": 12000 49 | } 50 | ], 51 | 'version': ts, 52 | 'reinit_date': ts, 53 | 'priority': 0, 54 | 'expire_at': 0, 55 | } 56 | 57 | value = client.schemas.serialize(client.schemas.get_by_name('adnl.addressList'), value_data) 58 | 59 | stored = await client.store_value( # store our address list in dht as value 60 | key=key, 61 | value=value, 62 | private_key=adnl.client.ed25519_private.encode(), 63 | ttl=100, 64 | try_find_after=False 65 | ) 66 | 67 | print(stored) # True if value was stored, false otherwise 68 | 69 | 70 | async def overlay(): 71 | """VERY RAW OVERLAY USAGE""" 72 | 73 | # look for basechain overlay nodes 74 | 75 | def get_overlay_key(): 76 | schemes = client.schemas 77 | sch = schemes.get_by_name('tonNode.shardPublicOverlayId') 78 | data = { 79 | "workchain": 0, 80 | "shard": -9223372036854775808, 81 | "zero_state_file_hash": "5e994fcf4d425c0a6ce6a792594b7173205f740a39cd56f537defd28b48a0f6e" 82 | } 83 | key_id = hashlib.sha256(schemes.serialize(sch, data)).digest() 84 | sch = schemes.get_by_name('pub.overlay') 85 | data = { 86 | 'name': key_id 87 | } 88 | key_id = schemes.serialize(sch, data) 89 | return client.get_dht_key_id_tl(id_=hashlib.sha256(key_id).digest(), name=b'nodes') 90 | 91 | resp = await client.find_value(key=get_overlay_key(), timeout=30) 92 | print(resp) 93 | # {'@type': 'dht.valueFound', 'value': {'key': {'key': {'id': '12b8a83f098e15ea47fe76d0b0df0986ff6dda1980796b084b0d2a68b2558649', 'name': b'nodes', 'idx': 0, '@type': 'dht.key'}, 'id': {'name': b'\x945\xc2\x12\xdc\x0e\xc5\x1d\xachd\x10\xe9\xba\x98\xf4\xb6\xfc}_\x08\xae\xb9\x16A\t\x17\x8e\xb9P\xdd\xec', '@type': 'pub.overlay'}, 'update_rule': {'@type': 'dht.updateRule.overlayNodes'}, 'signature': b'', '@type': 'dht.keyDescription'}, 'value': {'@type': 'overlay.nodes', 'nodes': [{'id': {'key': 'b557ef2a24d14aca18e129071b2e75562842f0bae1459669afb736f5990a3c61', '@type': 'pub.ed25519'}, 'overlay': '12b8a83f098e15ea47fe76d0b0df0986ff6dda1980796b084b0d2a68b2558649', 'version': 1695829307, 'signature': b'Z\x0e\xc9\xc0MA\xd7\xf5Z\xab\xc3\xa3\xd7\xd0\x96\x97\x8b\x05x5\xbd\xd7\xc4\xe7\xfa5\xd5\x06\xdb\xe2"\x0f>s8\x12\x93\xba\xae\xe5\x9eCI\xab\x98\xe9\x1dx4\x0c\xb4\x8d\xf3\x8e\x01\xdd\x15N\xa6/\x18\xfa\xaf\r'}, {'id': {'key': '6aa8baa5d300a70a83d901028842c8f1ff7244d6ff12c33faf08265949f7ca1d', '@type': 'pub.ed25519'}, 'overlay': '12b8a83f098e15ea47fe76d0b0df0986ff6dda1980796b084b0d2a68b2558649', 'version': 1695315522, 'signature': b'h\xe6\x1c\xfa\xa2\xf8\xf2\x80\xe5}\x15\xf0\x96\xc94N\xe4\xe2\xb6\xfe\xaf\tN8\x11D+\xfb\x95\x92\xe0Tn\xff\xd1\x81(e\xf0\\\x95@[{\xb1\xd4\xd1\xae\xe4&\xa2\xaa\x04\xb2,z%\xd3\nx\xd3h\xc4\t'}, {'id': {'key': 'bb626e1a95b117e8d45cfb9158f2b5e80dae75353f9959320e2af01ea147bc3c', '@type': 'pub.ed25519'}, 'overlay': '12b8a83f098e15ea47fe76d0b0df0986ff6dda1980796b084b0d2a68b2558649', 'version': 1678009360, 'signature': b'\n\x9a\x1b\n\xeb\x9e\x1a\xda\xb1z\xf3\xe7\xf1\x8c\xb6hH\xc5\x7fmc\x02Zv\xac"3\x01\x84Wf\x92@\x11\xa8\xb9\x92\x1f\x86N\xf5\xbc\x15\xca\xe7\xf0\x96i\xf7\xcc~&3\xc8\xb3tZ+\xac\xb9\xd9\x03\xd7\n'}, {'id': {'key': '6f8a107b1ab5ddf6ea286af2d5e29db703c921826ab3e19b1a039987e3baca8b', '@type': 'pub.ed25519'}, 'overlay': '12b8a83f098e15ea47fe76d0b0df0986ff6dda1980796b084b0d2a68b2558649', 'version': 1695829212, 'signature': b"\n\xffS\xf3r\xac@\xd2\xd4.m1\xe4j\xc15\xfai\xc7G\x0bB|L\x1c\xd2.\x98A\xb5\xcd\xe4mw\xc6\x14\xac\x18<\x16J_\xb6\xe8\x96k\xe2`\xe7S\xf2\x1c\xe7\xc2\xd2\x15'\x87\x7fjAo\x04\x05"}, {'id': {'key': '504da3252f966807a7855bdc75498c6dd6f16da384cea76076003dea7fd50a12', '@type': 'pub.ed25519'}, 'overlay': '12b8a83f098e15ea47fe76d0b0df0986ff6dda1980796b084b0d2a68b2558649', 'version': 1661512096, 'signature': b"\xbb\x17\x9c\x9d\x19QO\xcc\xd1\t\n\x11\x1d.\xf1\xad\xdf\xc1pL\xee\x01\x1b\xe3\xbc\x92OE,Nj\xf9\xac\x9d\x87\xb7\xbb;'\xeae\xc9\xef\xfe_9@\xa9,\x85}\xb7\xab\x13\xbfw\x1b\x8eg\xab\xc8\x08\x88\x08"}]}, 'ttl': 1695832907, 'signature': b'', '@type': 'dht.value'}} 94 | 95 | # choose node 96 | node = resp['value']['value']['nodes'][1] 97 | 98 | pub_k = bytes.fromhex(node['id']['key']) 99 | adnl_addr = Server('', 0, pub_key=pub_k).get_key_id() 100 | resp = await client.find_value(key=client.get_dht_key_id(id_=adnl_addr), timeout=20) # find overlay node 101 | print(resp) 102 | # {'@type': 'dht.valueFound', 'value': {'key': {'key': {'id': 'a63ece1e9dabe9348711339998a423553e9899fdd9c3e2ca6942686e7f4e34f6', 'name': b'address', 'idx': 0, '@type': 'dht.key'}, 'id': {'key': '6aa8baa5d300a70a83d901028842c8f1ff7244d6ff12c33faf08265949f7ca1d', '@type': 'pub.ed25519'}, 'update_rule': {'@type': 'dht.updateRule.signature'}, 'signature': b'P\x12\xcbWu9\xef\xf48\xd2\n\xb5\xb0\xc9\xa0\xfaLo\xc6\xbc\x96\xc8)I\xaa\x0e\xfcm\x1b\xf2jf\xea\xbe\xf4Gb\xcc!L\x9c9\xeb{\x13\xcd\x8c2<\xc7\xe5\x9cbxr\x80\xe4\xe0\xc7\xa4\xb7\xed\x01\x03', '@type': 'dht.keyDescription'}, 'value': {'@type': 'adnl.addressList', 'addrs': [{'@type': 'adnl.address.udp', 'ip': -1923101371, 'port': 30303}], 'version': 1695828878, 'reinit_date': 1695314720, 'priority': 0, 'expire_at': 0}, 'ttl': 1695832478, 'signature': b'.\xff\xed\x0brf\x13\x13\x8b\xf3\xb6\x85\xaaQ7dH\r\xbaq8m\x14\xf8?\xc7\xb0sJ!\x87@!*\xcd\x80{Y\x92\x17\xfd\xa3\x18fb\x8aD\xe2b``\xb5\xb7\x03u\xa3\xbe\x83\xdd\x9c\xf9\x1f,\x02', '@type': 'dht.value'}} 103 | 104 | 105 | if __name__ == '__main__': 106 | asyncio.run(main()) 107 | -------------------------------------------------------------------------------- /examples/adnl/dns.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import hashlib 3 | import logging 4 | 5 | from pytoniq_core import Address, Builder, Slice 6 | from pytoniq import LiteClient, AdnlTransport, DhtClient, Node, DhtNode 7 | 8 | 9 | client = LiteClient.from_mainnet_config(5, 2) 10 | 11 | 12 | async def main(): 13 | await client.connect() 14 | logging.basicConfig(level=logging.DEBUG) 15 | 16 | # resolve domain foundation.ton 17 | request_stack = [Builder().store_uint(0, 8).end_cell().begin_parse(), int.from_bytes(hashlib.sha256(b'site').digest(), 'big')] 18 | stack = await client.run_get_method(address='EQB43-VCmf17O7YMd51fAvOjcMkCw46N_3JMCoegH_ZDo40e', method='dnsresolve', stack=request_stack) 19 | await client.close() 20 | cs: Slice = stack[1].begin_parse() 21 | assert cs.load_bytes(2) == b'\xad\x01' 22 | adnl_addr = cs.load_bytes(32) 23 | 24 | print(adnl_addr.hex()) # foundation.ton node adnl address 25 | 26 | adnl = AdnlTransport(timeout=3) 27 | await adnl.start() 28 | 29 | dht_client = DhtClient.from_mainnet_config(adnl) 30 | 31 | value = await dht_client.find_value( 32 | key=DhtClient.get_dht_key_id(adnl_addr), 33 | timeout=15 34 | ) 35 | 36 | print(value) # dht.valueFound 37 | 38 | await dht_client.close() 39 | await adnl.close() 40 | 41 | asyncio.run(main()) 42 | -------------------------------------------------------------------------------- /examples/blocks/block_scanner.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from types import coroutine 3 | 4 | from pytoniq_core.tlb.block import ExtBlkRef 5 | 6 | from pytoniq.liteclient import LiteClient 7 | from pytoniq_core.tl import BlockIdExt 8 | 9 | 10 | class BlockScanner: 11 | 12 | def __init__(self, 13 | client: LiteClient, 14 | block_handler: coroutine 15 | ): 16 | """ 17 | :param client: LiteClient 18 | :param block_handler: function to be called on new block 19 | """ 20 | self.client = client 21 | self.block_handler = block_handler 22 | self.shards_storage = {} 23 | self.blks_queue = asyncio.Queue() 24 | 25 | async def run(self, mc_seqno: int = None): 26 | if not self.client.inited: 27 | raise Exception('should init client first') 28 | 29 | if mc_seqno is None: 30 | master_blk: BlockIdExt = self.mc_info_to_tl_blk(await self.client.get_masterchain_info()) 31 | else: 32 | master_blk, _ = await self.client.lookup_block(wc=-1, shard=-9223372036854775808, seqno=mc_seqno) 33 | 34 | master_blk_prev, _ = await self.client.lookup_block(wc=-1, shard=-9223372036854775808, seqno=master_blk.seqno - 1) 35 | 36 | shards_prev = await self.client.get_all_shards_info(master_blk_prev) 37 | for shard in shards_prev: 38 | self.shards_storage[self.get_shard_id(shard)] = shard.seqno 39 | 40 | while True: 41 | await self.blks_queue.put(master_blk) 42 | 43 | shards = await self.client.get_all_shards_info(master_blk) 44 | for shard in shards: 45 | await self.get_not_seen_shards(shard) 46 | self.shards_storage[self.get_shard_id(shard)] = shard.seqno 47 | 48 | while not self.blks_queue.empty(): 49 | await self.block_handler(self.blks_queue.get_nowait()) 50 | 51 | while True: 52 | if master_blk.seqno + 1 == self.client.last_mc_block.seqno: 53 | master_blk = self.client.last_mc_block 54 | break 55 | elif master_blk.seqno + 1 < self.client.last_mc_block.seqno: 56 | master_blk, _ = await self.client.lookup_block(wc=-1, shard=-9223372036854775808, seqno=master_blk.seqno + 1) 57 | break 58 | await asyncio.sleep(0.1) 59 | 60 | async def get_not_seen_shards(self, shard: BlockIdExt): 61 | if self.shards_storage.get(self.get_shard_id(shard)) == shard.seqno: 62 | return 63 | 64 | full_blk = await self.client.raw_get_block_header(shard) 65 | prev_ref = full_blk.info.prev_ref 66 | if prev_ref.type_ == 'prev_blk_info': # only one prev block 67 | prev: ExtBlkRef = prev_ref.prev 68 | prev_shard = self.get_parent_shard(shard.shard) if full_blk.info.after_split else shard.shard 69 | await self.get_not_seen_shards(BlockIdExt( 70 | workchain=shard.workchain, seqno=prev.seqno, shard=prev_shard, 71 | root_hash=prev.root_hash, file_hash=prev.file_hash 72 | ) 73 | ) 74 | else: 75 | prev1: ExtBlkRef = prev_ref.prev1 76 | prev2: ExtBlkRef = prev_ref.prev2 77 | await self.get_not_seen_shards(BlockIdExt( 78 | workchain=shard.workchain, seqno=prev1.seqno, shard=self.get_child_shard(shard.shard, left=True), 79 | root_hash=prev1.root_hash, file_hash=prev1.file_hash 80 | ) 81 | ) 82 | await self.get_not_seen_shards(BlockIdExt( 83 | workchain=shard.workchain, seqno=prev2.seqno, shard=self.get_child_shard(shard.shard, left=False), 84 | root_hash=prev2.root_hash, file_hash=prev2.file_hash 85 | ) 86 | ) 87 | 88 | await self.blks_queue.put(shard) 89 | 90 | def get_child_shard(self, shard: int, left: bool) -> int: 91 | x = self.lower_bit64(shard) >> 1 92 | if left: 93 | return self.simulate_overflow(shard - x) 94 | return self.simulate_overflow(shard + x) 95 | 96 | def get_parent_shard(self, shard: int) -> int: 97 | x = self.lower_bit64(shard) 98 | return self.simulate_overflow((shard - x) | (x << 1)) 99 | 100 | @staticmethod 101 | def simulate_overflow(x) -> int: 102 | return (x + 2**63) % 2**64 - 2**63 103 | 104 | @staticmethod 105 | def lower_bit64(num: int) -> int: 106 | return num & (~num + 1) 107 | 108 | @staticmethod 109 | def mc_info_to_tl_blk(info: dict): 110 | return BlockIdExt.from_dict(info['last']) 111 | 112 | @staticmethod 113 | def get_shard_id(blk: BlockIdExt): 114 | return f'{blk.workchain}:{blk.shard}' 115 | 116 | 117 | async def handle_block(block: BlockIdExt): 118 | if block.workchain == -1: # skip masterchain blocks 119 | return 120 | print(block) 121 | transactions = await client.raw_get_block_transactions_ext(block) 122 | print(f"{len(transactions)=}") 123 | # for transaction in transactions: 124 | # print(transaction.in_msg) 125 | 126 | 127 | client = LiteClient.from_mainnet_config(ls_i=14, trust_level=2, timeout=20) 128 | 129 | 130 | async def main(): 131 | 132 | await client.connect() 133 | await client.reconnect() 134 | await BlockScanner(client=client, block_handler=handle_block).run() 135 | 136 | 137 | if __name__ == '__main__': 138 | asyncio.run(main()) 139 | -------------------------------------------------------------------------------- /examples/blocks/block_transactios.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from pytoniq import LiteClient, WalletV4R2, Address 4 | 5 | 6 | async def main(): 7 | client = LiteClient.from_mainnet_config(ls_i=0, trust_level=2) 8 | 9 | await client.connect() 10 | 11 | result = await client.raw_get_block_transactions(block=client.last_shard_blocks[0]) 12 | print(result) # [{'account': Address, 'lt': 39525375000001, 'hash': b'{U44\xff\xcdp/VB\n1[\x12\xdf\x86\x88\xc8re\xcb\xc0o\xb2)\x9c]\xbe\xf9\xc4ta'}, {'account': Address, 'lt': 39525375000003, 'hash': b'\xde5\x04\x81f\xce(i\xcb\x8f\x8f\xfd\\\x0b\xe1q2\x85\xe4-\x97^\x03\xa9j#\xb5|\x9f5\xd3\x00'}, {'account': Address, 'lt': 39525375000003, 'hash': b'\xc1\xeemG\xee\xe7\xe2{\x1c\x0c\x10\x9a\x14\x0f\x0b$\xc7\xf0\t_E?\x94`Q4\x1a\xcd\xd4\xe1\xfb\xee'}, {'account': Address, 'lt': 39525375000001, 'hash': b'&Y1\xd9\xa5\xa2{\xa7\xd94\xe4\x011\xeb\x1f\xe4\xa4\x17)\xf0\nl\x04z\xdbt\n\x96\x8d\xe6\xc8\x1d'}, {'account': Address, 'lt': 39525375000005, 'hash': b'O\xf3\xe9O\x86P#\xd7\x918\x87;\x9a\xad1`fm\x06u\x0e\x9bW\x9a\x9c\x17w\xa8*C\xffd'}] 13 | 14 | result = await client.raw_get_block_transactions_ext(block=client.last_shard_blocks[0]) 15 | print(result) # [{'account_addr': '0816e9c02d110e790f7db3231b3bab12cccfc56b546c3c3e530684b4a9578a43', 'lt': 39525416000005, 'prev_trans_hash': b'B\x00R\xa1\x80\xc0\xfev\xd6\x1b\xb0\x8d\xdd\x8b\x8b_[\xda&\xb6h-J*B\xbd\xb5\xfeZ\xfb\xf2\xc2', 'prev_trans_lt': 39525408000005, 'now': 1690132248, 'outmsg_cnt': 2, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 1000000000, 'other': None}, 'value_coins': 1000000000, 'ihr_fee': 0, 'fwd_fee': 6584717, 'created_lt': 39525416000004, 'created_at': 1690132248}, 'init': None, 'body': 2 refs>}, 'out_msgs': [{'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 20000000, 'other': None}, 'value_coins': 20000000, 'ihr_fee': 0, 'fwd_fee': 6254715, 'created_lt': 39525416000006, 'created_at': 1690132248}, 'init': {'split_depth': None, 'special': None, 'code': 1 refs>, 'data': 1 refs>, 'library': None}, 'body': 0 refs>}, {'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 963158000, 'other': None}, 'value_coins': 963158000, 'ihr_fee': 0, 'fwd_fee': 1157343, 'created_lt': 39525416000007, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}], 'total_fees': {'grams': 9428945, 'other': None}, 'state_update': {'old_hash': b'A\xb9K3}, {'account_addr': '122a594e9599a4aa32faba392548d4f593103cbb962180407bc7139eb103daf7', 'lt': 39525416000001, 'prev_trans_hash': b'iD\x01\x11\xfd\xbcS{\xed=c\x87\x96\xd5dOp\x8a\xc3Z\x9f@\x01Q\xaeP\xf1r\xad=\x96W', 'prev_trans_lt': 39525382000001, 'now': 1690132248, 'outmsg_cnt': 1, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'src': None, 'dest': Address, 'import_fee': 0}, 'init': None, 'body': 1 refs>}, 'out_msgs': [{'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 5786900000000, 'other': None}, 'value_coins': 5786900000000, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000002, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}], 'total_fees': {'grams': 4859333, 'other': None}, 'state_update': {'old_hash': b'\x05\x0f\xd3\xf8g\x9a\x9a\x97\x98\xaa~Csa\xc3\x1d\xa2\x8e\x94\xc1\xdf\xac\x9fI\x9a\xf2\xf1v\xfa\xfe\x9b\xed', 'new_hash': b'\xb9M4q}8!\x12\xf9l\x86\xfa\xb9\xc0\xb2hc\xe9\xb1>\xe4\x04"\x9d\xc5\xad\x03\xf4\x16:\x90\xd3'}, 'description': {'type_': 'ordinary', 'credit_first': True, 'storage_ph': {'storage_fees_collected': 5, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': None, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 2994000, 'gas_used': 2994, 'gas_limit': None, 'gas_credit': 10000, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 66, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': 1000000, 'total_action_fees': 333328, 'result_code': 0, 'result_arg': None, 'tot_actions': 1, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 1, 'action_list_hash': b'CKz]\x18DA\xae\xf4\xf9\xf80\x1cd\xe9\xc3\x19A \x9e\x8efM\xf3\x83\xac\x82\xff\x9d\x9c\x8d\xa8', 'tot_msg_size': {'cells': 1, 'bits': 721}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': '4a1028b7dc2a99f9cd9985d299c21db79f9bf68476651485c86f7eb08c99a1e4', 'lt': 39525416000001, 'prev_trans_hash': b'ff\xceW\xd6Kx\x8a`t|\xa7$F\x063\xbf\xb7\x14s#\xccX\xd2/\xb8\xbb\xb7T\x03\xdd\xd1', 'prev_trans_lt': 39525406000010, 'now': 1690132248, 'outmsg_cnt': 1, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'src': None, 'dest': Address, 'import_fee': 0}, 'init': None, 'body': 1 refs>}, 'out_msgs': [{'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 1038572001, 'other': None}, 'value_coins': 1038572001, 'ihr_fee': 0, 'fwd_fee': 6787386, 'created_lt': 39525416000002, 'created_at': 1690132248}, 'init': None, 'body': 2 refs>}], 'total_fees': {'grams': 17398623, 'other': None}, 'state_update': {'old_hash': b'\x93H\xae\x90+\xd3\xc4Lq\xb5:\xaf\x168\x93\x8c\xefl$\xffx\xd3#\x8c1\xc0i\x0f7lR\xff', 'new_hash': b'\xe0\xdeQ\xbb\r\xb5fi\x92\xd63KY&\x9a\r$\xect\xa5w\xc0P\x16\x84\xd8-\xc4P\xee)\x10'}, 'description': {'type_': 'ordinary', 'credit_first': True, 'storage_ph': {'storage_fees_collected': 9, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': None, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 3308000, 'gas_used': 3308, 'gas_limit': None, 'gas_credit': 10000, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 68, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': 10181000, 'total_action_fees': 3393614, 'result_code': 0, 'result_arg': None, 'tot_actions': 1, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 1, 'action_list_hash': b"\x0eS\xe0K\xde\xd5\xc0T\xefi\x9c\xdb\xa9)\x15E\x9e\x085'\xe9\xe6,\xec\r\x8e\xc4\x84\x1e,\xafz", 'tot_msg_size': {'cells': 17, 'bits': 8286}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': '4a1028b7dc2a99f9cd9985d299c21db79f9bf68476651485c86f7eb08c99a1e4', 'lt': 39525416000006, 'prev_trans_hash': b'Q&i\x91`\x05\xf9\xc0\xef\xec\xe4i\xfa\x0f/Y\xdfh\x8e\t\xb7\xe2\xb7d\x85\x89\xf8\x85\xe6\xefa\xd2', 'prev_trans_lt': 39525416000001, 'now': 1690132248, 'outmsg_cnt': 0, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 18266152, 'other': None}, 'value_coins': 18266152, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000005, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}, 'out_msgs': None, 'total_fees': {'grams': 991000, 'other': None}, 'state_update': {'old_hash': b'\xe0\xdeQ\xbb\r\xb5fi\x92\xd63KY&\x9a\r$\xect\xa5w\xc0P\x16\x84\xd8-\xc4P\xee)\x10', 'new_hash': b'\xd8I\x1f\x86\xdcA\xb6\xe9\xef\xc6\x95a\x9e\x1fw#\xdb\x1ab\x0b;\x13\x0f"gmn\xf1\xd6\x9e\xf8\xc8'}, 'description': {'type_': 'ordinary', 'credit_first': True, 'storage_ph': {'storage_fees_collected': 0, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 18266152, 'other': None}}, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 991000, 'gas_used': 991, 'gas_limit': 18266, 'gas_credit': None, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 29, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': None, 'total_action_fees': None, 'result_code': 0, 'result_arg': None, 'tot_actions': 0, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 0, 'action_list_hash': b'\x96\xa2\x96\xd2$\xf2\x85\xc6{\xee\x93\xc3\x0f\x8a0\x91W\xf0\xda\xa3]\xc5\xb8~A\x0bxc\n\t\xcf\xc7', 'tot_msg_size': {'cells': None, 'bits': None}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': '4a1028b7dc2a99f9cd9985d299c21db79f9bf68476651485c86f7eb08c99a1e4', 'lt': 39525416000010, 'prev_trans_hash': b'r\xb5\x7f\xbb\xc1\xa8\x05\x84v\xba\xf5\xc5W7\x1e\x83@\xf4\x07\xa3,\x1b\xef\xae8@\x9cP\x87\xff\xd4\x8f', 'prev_trans_lt': 39525416000006, 'now': 1690132248, 'outmsg_cnt': 0, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 956126429, 'other': None}, 'value_coins': 956126429, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000009, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}, 'out_msgs': None, 'total_fees': {'grams': 991000, 'other': None}, 'state_update': {'old_hash': b'\xd8I\x1f\x86\xdcA\xb6\xe9\xef\xc6\x95a\x9e\x1fw#\xdb\x1ab\x0b;\x13\x0f"gmn\xf1\xd6\x9e\xf8\xc8', 'new_hash': b"y\xde\xe9\x05n':7%\xac\xc2\x0e>rU\xdfF\x8a\x02\xd3\x98\x84\xac\xc9t*\x86\x86\xf9sHZ"}, 'description': {'type_': 'ordinary', 'credit_first': True, 'storage_ph': {'storage_fees_collected': 0, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 956126429, 'other': None}}, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 991000, 'gas_used': 991, 'gas_limit': 956126, 'gas_credit': None, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 29, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': None, 'total_action_fees': None, 'result_code': 0, 'result_arg': None, 'tot_actions': 0, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 0, 'action_list_hash': b'\x96\xa2\x96\xd2$\xf2\x85\xc6{\xee\x93\xc3\x0f\x8a0\x91W\xf0\xda\xa3]\xc5\xb8~A\x0bxc\n\t\xcf\xc7', 'tot_msg_size': {'cells': None, 'bits': None}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': '5ac8cc529e92bbe829729d38b3e3a85defbb53cfdd6001133b450c9671c3ab8d', 'lt': 39525416000003, 'prev_trans_hash': b'\xcfX\x80\xf8\x88\x84&\xa1r\x98\xde1.k\xd2\x9aq\xc0b\x98\xa0\xfa\xa1Z\xac\xf0M\x02\xb5c\x18\xcf', 'prev_trans_lt': 39339100000005, 'now': 1690132248, 'outmsg_cnt': 1, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 11000000000, 'other': None}, 'value_coins': 11000000000, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000002, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}, 'out_msgs': [{'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 10000000000, 'other': None}, 'value_coins': 10000000000, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000004, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}], 'total_fees': {'grams': 13480099, 'other': None}, 'state_update': {'old_hash': b'{w\x1e\xd5\xa9\xcbS@e\x94Vh=c\xf0V$\x1d\x87\xe7\xb7A\xb7\xe7\x10\x8a\x0f\xf4(\xe3\x84-', 'new_hash': b'\xbd?\x9bWV\xe6\xc9\xca<\xd5\x00\xbfI\x1d\x90\x939j8Q\xc6\xed\x995\x03\xd8,\xfd\x92\xb7\xba\xf6'}, 'description': {'type_': 'ordinary', 'credit_first': False, 'storage_ph': {'storage_fees_collected': 432771, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 11000000000, 'other': None}}, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 12714000, 'gas_used': 12714, 'gas_limit': 1000000, 'gas_credit': None, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 311, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': 1000000, 'total_action_fees': 333328, 'result_code': 0, 'result_arg': None, 'tot_actions': 1, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 1, 'action_list_hash': b'\np\xa3N\x89\xc6\xda\x8c\x82KG\xdb\r\xf1\xb2\x1ah@\xc5\x94,\xbawuJ\xb8\xff\xa4.\xfa\xa0\x9e', 'tot_msg_size': {'cells': 1, 'bits': 809}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': '601a70f9c436b185cd44959477d94da568e9b58ec61d088ad8329cb155f475ba', 'lt': 39525416000003, 'prev_trans_hash': b'\x96\x18\xeb\xc9\xcc\xf6\x99Z\xf3\x99\xae\x97\\\xf8\x06\xbeCH\x83|K\xe0aO\xed\xf6\x80\xf1\xbc\xe8\xf0\xfb', 'prev_trans_lt': 39276368000008, 'now': 1690132248, 'outmsg_cnt': 2, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 1038572001, 'other': None}, 'value_coins': 1038572001, 'ihr_fee': 0, 'fwd_fee': 6787386, 'created_lt': 39525416000002, 'created_at': 1690132248}, 'init': None, 'body': 2 refs>}, 'out_msgs': [{'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 1000000000, 'other': None}, 'value_coins': 1000000000, 'ihr_fee': 0, 'fwd_fee': 6584717, 'created_lt': 39525416000004, 'created_at': 1690132248}, 'init': None, 'body': 2 refs>}, {'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 18266152, 'other': None}, 'value_coins': 18266152, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000005, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}], 'total_fees': {'grams': 12347031, 'other': None}, 'state_update': {'old_hash': b"\xac(\xe1)\x0b\x85\xe0\x98\x15\xd5\xee\xdd\xd4\x84%\xef\x1a\xd4\xa9\xdb\xcf/\xf2\x1a\xe7\x7f\x83\xcf'\x96^O", 'new_hash': b')\xdc\x08\x08\xca\xe8\xcf+\xf9\xa0^"R\xd0\xdcp\xc47M\xf4TG\xd4d\xb9\x1b)\x11\x9aF\xf8G'}, 'description': {'type_': 'ordinary', 'credit_first': False, 'storage_ph': {'storage_fees_collected': 149420, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 1038572001, 'other': None}}, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 8572000, 'gas_used': 8572, 'gas_limit': 1000000, 'gas_credit': None, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 214, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': 10877000, 'total_action_fees': 3625611, 'result_code': 0, 'result_arg': None, 'tot_actions': 2, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 2, 'action_list_hash': b'}Y\xc3\xe1\xd2\x13\xe3\xc2.\x8f\x08\x81*\x1b5K\x10@\x19\x96F\xc6\x97\x0ch\xfa\xc8\x13%\xfe)1', 'tot_msg_size': {'cells': 18, 'bits': 8783}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': '601a70f9c436b185cd44959477d94da568e9b58ec61d088ad8329cb155f475ba', 'lt': 39525416000008, 'prev_trans_hash': b'O\xd5d\x83\x95;\x97\xc8\x176,\xdc\xc4\x179\x06\x0b\x18\xde\xfbf\x1a\xc9\x11bQ\x8f_!\xbbA\xce', 'prev_trans_lt': 39525416000003, 'now': 1690132248, 'outmsg_cnt': 1, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 963158000, 'other': None}, 'value_coins': 963158000, 'ihr_fee': 0, 'fwd_fee': 1157343, 'created_lt': 39525416000007, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}, 'out_msgs': [{'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 956126429, 'other': None}, 'value_coins': 956126429, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000009, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}], 'total_fees': {'grams': 7072328, 'other': None}, 'state_update': {'old_hash': b')\xdc\x08\x08\xca\xe8\xcf+\xf9\xa0^"R\xd0\xdcp\xc47M\xf4TG\xd4d\xb9\x1b)\x11\x9aF\xf8G', 'new_hash': b'\x10z\xd3\x8b\xb4\xb1\x03T4\xa2\x0cPx\x11\xb2\xb5\xee\x97\xefi\xab\xc7\xdc\x89t\xe8\xfb\xf4Y\xab\xa9\xf1'}, 'description': {'type_': 'ordinary', 'credit_first': False, 'storage_ph': {'storage_fees_collected': 0, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 963158000, 'other': None}}, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 6739000, 'gas_used': 6739, 'gas_limit': 963158, 'gas_credit': None, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 175, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': 1000000, 'total_action_fees': 333328, 'result_code': 0, 'result_arg': None, 'tot_actions': 1, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 1, 'action_list_hash': b"\xddx\xcb\xb5]\x8d\xc1v\xb5\x15y^':\xa8eav\x7f\xe0\xe5\xaf\x05\x818\xc4UI\x05\xa7\x92\xe9", 'tot_msg_size': {'cells': 1, 'bits': 801}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': '66eb31d95242593d83363e2fd85dd6fac1e1efbf00998169907d1f67aa011070', 'lt': 39525416000005, 'prev_trans_hash': b'\xd9M\x96\xca2\xa3\x8b\x8ag\x8a\xdcL\xf9\xe1\xcb\x0bO\xe4;\x07g\xa3\x0c\x04_\x1c\x9d\xea\x9e&\x07O', 'prev_trans_lt': 39525399000005, 'now': 1690132248, 'outmsg_cnt': 0, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 10000000000, 'other': None}, 'value_coins': 10000000000, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000004, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}, 'out_msgs': None, 'total_fees': {'grams': 991014, 'other': None}, 'state_update': {'old_hash': b'\xb5i4\xf66K\xee\xe0\x98O;n!\x08\x81\x93\xc3{\x01\x16\xcfj\x1fE\x0e\xd4\x13\x1b\xa8)\x85\x04', 'new_hash': b'\xf7;\xc5\xae\xb3\x17,5Q8E\x0fG\x15\x14r\xeb\xe6\xea0\xdd\x9f+\x1f7>V\x8c_e9y'}, 'description': {'type_': 'ordinary', 'credit_first': True, 'storage_ph': {'storage_fees_collected': 14, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 10000000000, 'other': None}}, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 991000, 'gas_used': 991, 'gas_limit': 1000000, 'gas_credit': None, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 29, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': None, 'total_action_fees': None, 'result_code': 0, 'result_arg': None, 'tot_actions': 0, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 0, 'action_list_hash': b'\x96\xa2\x96\xd2$\xf2\x85\xc6{\xee\x93\xc3\x0f\x8a0\x91W\xf0\xda\xa3]\xc5\xb8~A\x0bxc\n\t\xcf\xc7', 'tot_msg_size': {'cells': None, 'bits': None}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': 'a2275a350805dc7f46084ee7ffbbad222c9984c2ad98d8346e4a711e32f3f07d', 'lt': 39525416000003, 'prev_trans_hash': b'\xc6\x17\xa0\xcf\x8b\xca\xf7\xce\xa4e`A(\xc3|\xc5\xda\x03\x17{`\xb7\xf2\x86\x89q\xaf\xe4\x086\xff\xa1', 'prev_trans_lt': 39516700000001, 'now': 1690132248, 'outmsg_cnt': 0, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 5786900000000, 'other': None}, 'value_coins': 5786900000000, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000002, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}, 'out_msgs': None, 'total_fees': {'grams': 101223, 'other': None}, 'state_update': {'old_hash': b'8H\x17Vp,B\x0c\xcf\x18,\x8f\x1f2RG\xd4\x8fI\x14\x8e\x83\x914I\xf5[l\x00AQm', 'new_hash': b'\x19a-\xa9\xaa.\xe9C$\xb3?\x00\xbd\xa4^\xa3\xb5O\x00\xe5l0`\xef\x83`\x06\xfe)\xb6\x8f3'}, 'description': {'type_': 'ordinary', 'credit_first': True, 'storage_ph': {'storage_fees_collected': 1223, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 5786900000000, 'other': None}}, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 100000, 'gas_used': 62, 'gas_limit': 1000000, 'gas_credit': None, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 3, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': None, 'total_action_fees': None, 'result_code': 0, 'result_arg': None, 'tot_actions': 0, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 0, 'action_list_hash': b'\x96\xa2\x96\xd2$\xf2\x85\xc6{\xee\x93\xc3\x0f\x8a0\x91W\xf0\xda\xa3]\xc5\xb8~A\x0bxc\n\t\xcf\xc7', 'tot_msg_size': {'cells': None, 'bits': None}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': 'e2ee8b1f194a8b04bc113f1db0061a5f1f8faf622202d96856b55a28faeb6d78', 'lt': 39525416000007, 'prev_trans_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'prev_trans_lt': 0, 'now': 1690132248, 'outmsg_cnt': 0, 'orig_status': {'type_': 'nonexist'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 20000000, 'other': None}, 'value_coins': 20000000, 'ihr_fee': 0, 'fwd_fee': 6254715, 'created_lt': 39525416000006, 'created_at': 1690132248}, 'init': {'split_depth': None, 'special': None, 'code': 1 refs>, 'data': 1 refs>, 'library': None}, 'body': 0 refs>}, 'out_msgs': None, 'total_fees': {'grams': 1663000, 'other': None}, 'state_update': {'old_hash': b'\x90\xae\xc8\x96Z\xfa\xbb\x16\xeb\xc3\xcb\x9b@\x8e\xba\xe7\x1ba\x8dxx\x8b\xc8\r\t\x845\x93\xca\xc9\x8d\xa4', 'new_hash': b'\xd5\xfd\xcdMz@\x8a\xb6\xddK\xa9\xa9)*O\xb8Q\x0b\x08\xf1E\x97\xb7\nZK1BtZ/\x17'}, 'description': {'type_': 'ordinary', 'credit_first': False, 'storage_ph': {'storage_fees_collected': 0, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 20000000, 'other': None}}, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 1663000, 'gas_used': 1663, 'gas_limit': 20000, 'gas_credit': None, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 51, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': None, 'total_action_fees': None, 'result_code': 0, 'result_arg': None, 'tot_actions': 0, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 0, 'action_list_hash': b'\x96\xa2\x96\xd2$\xf2\x85\xc6{\xee\x93\xc3\x0f\x8a0\x91W\xf0\xda\xa3]\xc5\xb8~A\x0bxc\n\t\xcf\xc7', 'tot_msg_size': {'cells': None, 'bits': None}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}, {'account_addr': 'e8bed504b877da56ff6d89ea0bcfac163c5b770e089ad3e375ef7bcd4322b53d', 'lt': 39525416000001, 'prev_trans_hash': b'\xad\x01\x17O\xd2_he\x0e(\xa0\x13\x9a#J\x89\xaf\xa1\xf6\xedTP]\xb7\xc6\xfa\x80H, 'import_fee': 0}, 'init': None, 'body': 1 refs>}, 'out_msgs': [{'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 11000000000, 'other': None}, 'value_coins': 11000000000, 'ihr_fee': 0, 'fwd_fee': 666672, 'created_lt': 39525416000002, 'created_at': 1690132248}, 'init': None, 'body': 0 refs>}], 'total_fees': {'grams': 5165592, 'other': None}, 'state_update': {'old_hash': b'\xc9\xab\x0e\x0c\xda\xdf`\x8a\xf7\xdfd\xb2\xa2R\xcb8\xda\xa9\x14\xb8\\\xee*\xc8e\xa5?\xb1^\xcb\x8d\x08', 'new_hash': b'6:\t\xf0\xc2+\xa1W\x8f^\xfbM\xd3\x00\x1a\x91\xd6:\x90c\xb8\xc5\x9d\t}\xc3\xfb\xb9\xa8\x91\x97\xb9'}, 'description': {'type_': 'ordinary', 'credit_first': True, 'storage_ph': {'storage_fees_collected': 264, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': None, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 3308000, 'gas_used': 3308, 'gas_limit': None, 'gas_credit': 10000, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 68, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': 1000000, 'total_action_fees': 333328, 'result_code': 0, 'result_arg': None, 'tot_actions': 1, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 1, 'action_list_hash': b'\xa8\xc9"Q\xb8Y\x8aL\xc5\x83\xea\xefP2T\xb2\xea/\xd4\xb6\x9d\x86\xbf\xb9\xf8\xfd\xd8\x81\xb1\t\x8b\x04', 'tot_msg_size': {'cells': 1, 'bits': 713}}, 'aborted': False, 'bounce': None, 'destroyed': False}, 'account': Address}] 16 | 17 | transaction = result[0] 18 | print(transaction.in_msg) # {'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 50000000, 'other': None}, 'value_coins': 50000000, 'ihr_fee': 0, 'fwd_fee': 4496035, 'created_lt': 39525434000004, 'created_at': 1690132310}, 'init': {'split_depth': None, 'special': None, 'code': 1 refs>, 'data': 0 refs>, 'library': None}, 'body': 1 refs>} 19 | in_body = transaction.in_msg.body.begin_parse() 20 | print(in_body) 21 | # 96[487A8E815E80E42D86BB9E48] -> { 22 | # 395[800ACA0F7ACBD28C27DD4F2130EA7FFABFCAA12C0BF70A29B74D70AAFB5FC4CFC7EC09184E72A00000A00001C20001275000] 23 | # } 24 | 25 | 26 | if __name__ == '__main__': 27 | asyncio.run(main()) 28 | -------------------------------------------------------------------------------- /examples/extra_currency.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pytoniq import LiteBalancer, WalletV3R2, Address, Cell, WalletMessage, begin_cell, HighloadWalletV3 3 | from pytoniq_core.tlb.block import CurrencyCollection, ExtraCurrencyCollection 4 | from pytoniq_core.crypto.ciphers import get_random 5 | 6 | 7 | async def send_ec(): 8 | async with LiteBalancer.from_testnet_config(trust_level=2) as client: 9 | mnemo = [] 10 | wallet = await WalletV3R2.from_mnemonic(client, mnemo) 11 | 12 | addr = Address('EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c') 13 | currency_id = 100 14 | amount = 1*10**8 15 | body = begin_cell().store_uint(0, 32).store_snake_string('123456').end_cell() # comment 123456 16 | 17 | value = CurrencyCollection(grams=0, other=ExtraCurrencyCollection({currency_id: amount})) 18 | message = WalletV3R2.create_internal_msg(dest=addr, value=value, body=body) 19 | msg = WalletMessage(send_mode=3, message=message) 20 | await wallet.raw_transfer(msgs=[msg]) 21 | 22 | 23 | async def receive_ec(addr: str): 24 | async with LiteBalancer.from_testnet_config(trust_level=2) as client: 25 | ec_id = 100 26 | last_lt = 0 27 | while True: 28 | trs = await client.get_transactions(address=addr, count=16) 29 | if last_lt == 0: 30 | last_lt = trs[0].lt 31 | for tr in trs[::-1]: 32 | if tr.lt <= last_lt: 33 | continue 34 | last_lt = tr.lt 35 | if not tr.in_msg.is_internal: 36 | continue 37 | ec_dict = tr.in_msg.info.value.other.dict 38 | if ec_dict is not None and ec_dict.get(ec_id, 0) != 0: 39 | cs = tr.in_msg.body.begin_parse() 40 | if cs.remaining_bits >= 32 and cs.load_uint(32) == 0: 41 | comment = cs.load_snake_string() 42 | else: 43 | print('no comment in tr') 44 | continue 45 | if tr.description.bounce and tr.description.bounce.type_ == 'ok': 46 | print('bounced tr') 47 | continue 48 | print(f'Received {ec_dict[ec_id]} EC from {tr.in_msg.info.src} with comment: {comment}') 49 | return {'amount': ec_dict[ec_id], 'from': tr.in_msg.info.src} 50 | await asyncio.sleep(1) 51 | 52 | 53 | async def send_ec_highload(): 54 | async with LiteBalancer.from_testnet_config(trust_level=2) as client: 55 | # send 1000 messages to random addresses, each message has 1 nano EC and 0 ton attached with empty body 56 | mnemo = [] 57 | wallet = await HighloadWalletV3.from_mnemonic(client, mnemo) 58 | 59 | currency_id = 100 60 | amount = 1 61 | 62 | value = CurrencyCollection(grams=0, other=ExtraCurrencyCollection({currency_id: amount})) 63 | msgs = [] 64 | for i in range(1000): 65 | message = wallet.create_internal_msg(dest=Address((0, get_random(32))), value=value, body=Cell.empty()) 66 | msg = WalletMessage(send_mode=3, message=message) 67 | msgs.append(msg) 68 | await wallet.raw_transfer(msgs=msgs) 69 | 70 | 71 | if __name__ == '__main__': 72 | asyncio.run(send_ec()) 73 | asyncio.run(receive_ec('addr')) 74 | asyncio.run(send_ec_highload()) 75 | -------------------------------------------------------------------------------- /examples/get_methods.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from pytoniq_core import Address 5 | 6 | from pytoniq import LiteBalancer, WalletV4R2, LiteClient 7 | 8 | 9 | async def main(): 10 | logging.basicConfig(level=logging.INFO) 11 | client = LiteBalancer.from_mainnet_config(trust_level=1) 12 | 13 | await client.start_up() 14 | 15 | """wallet seqno""" 16 | result = await client.run_get_method(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', method='seqno', stack=[]) 17 | print(result) # [242] 18 | wallet = await WalletV4R2.from_address(provider=client, address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG') 19 | print(wallet.seqno) # 242 20 | print(await wallet.get_seqno()) # 242 21 | print(await wallet.run_get_method(method='seqno', stack=[])) # [242] 22 | 23 | """dex router get method""" 24 | result = await client.run_get_method(address='EQB3ncyBUTjZUA5EnFKR5_EnOMI9V1tTEAAPaiU71gc4TiUt', method='get_router_data', stack=[]) 25 | print(result) # [0, 0 refs>, 1 refs>, 1 refs>, 1 refs>, 1 refs>] 26 | print(result[1].load_address()) # EQBJm7wS-5M9SmJ3xLMCj8Ol-JKLikGDj-GfDwL1_6b7cENC 27 | 28 | """jetton wallets""" 29 | owner_address = Address('EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG') 30 | request_stack = [owner_address.to_cell().begin_parse()] 31 | result = await client.run_get_method(address='EQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwiuA', method='get_wallet_address', stack=request_stack) 32 | print(result) # [ 0 refs>] 33 | jetton_wallet_address = result[0].load_address() 34 | print(jetton_wallet_address) # EQDapqw6EnsabFZO46A4nIUXXtT4IIcnjPuabomeT4m3paST 35 | 36 | result = await client.run_get_method(address='EQDapqw6EnsabFZO46A4nIUXXtT4IIcnjPuabomeT4m3paST', method='get_wallet_data', stack=[]) 37 | print(result) # [2005472, 0 refs>, 0 refs>, 1 refs>] 38 | 39 | await client.close_all() 40 | 41 | """can run get method for any block liteserver remembers""" 42 | client = LiteClient.from_mainnet_config(2, 2) # archive liteserver 43 | await client.connect() 44 | blk, _ = await client.lookup_block(wc=0, shard=-2**63, seqno=33000000) 45 | result = await client.run_get_method(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', method='seqno', stack=[], block=blk) 46 | await client.close() 47 | print(result) 48 | 49 | if __name__ == '__main__': 50 | asyncio.run(main()) 51 | -------------------------------------------------------------------------------- /examples/transactions.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from pytoniq import LiteClient, MessageAny 4 | 5 | 6 | async def main(): 7 | async with LiteClient.from_mainnet_config(ls_i=0, trust_level=2) as client: 8 | 9 | trs = await client.get_transactions(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', count=3) 10 | print(trs) # [{'account_addr': '6f5bc67986e06430961d9df00433926a4cd92e597ddd8aa6043645ac20bd1782', 'lt': 39515024000004, 'prev_trans_hash': b'\xe93\xfaM"\xc6Cz\x1a\x8a\xfad\x8fe:\xf9sP!\xa4\x8bG\x02KJm^)w\x02Vq', 'prev_trans_lt': 39515024000003, 'now': 1690098531, 'outmsg_cnt': 0, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 1000, 'other': None}, 'value_coins': 1000, 'ihr_fee': 0, 'fwd_fee': 733339, 'created_lt': 39515024000003, 'created_at': 1690098531}, 'init': None, 'body': 0 refs>}, 'out_msgs': None, 'total_fees': {'grams': 0, 'other': None}, 'state_update': {'old_hash': b'\xd6U\xcc\x0bD\xf2\xaeh\xcf\xe8\xb7I\xa8v\x1a9\xee\x87\x12\xa0\xdb#\xe4\xc3\xa5\xf3\xf5\xae&\x01\xc7\xa7', 'new_hash': b"\x05\xa9\x7fx\xac\r'\xe2\x0e\xddX\xd2M\xe7\xd4\x19ZG\x08J\xe0\r\xeaW\xdd\xf5\xbdX\x86\x82\xf6*"}, 'description': {'type_': 'ordinary', 'credit_first': False, 'storage_ph': {'storage_fees_collected': 0, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 1000, 'other': None}}, 'compute_ph': {'type_': 'skipped', 'reason': {'type_': 'no_gas'}}, 'action': None, 'aborted': True, 'bounce': {'type_': 'nofunds', 'msg_size': {'cells': None, 'bits': None}, 'req_fwd_fees': 1000000}, 'destroyed': False}}, {'account_addr': '6f5bc67986e06430961d9df00433926a4cd92e597ddd8aa6043645ac20bd1782', 'lt': 39515024000003, 'prev_trans_hash': b'>y\xa3L\xd9\x15\x92\xc0P\x93\x8a\x86D`H\xa0\x87Z}S\xb7\xc1h\x97\x1cL\xe3\xf7\xb1\x1f\xa4\x8e', 'prev_trans_lt': 39514911000001, 'now': 1690098531, 'outmsg_cnt': 0, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 1000, 'other': None}, 'value_coins': 1000, 'ihr_fee': 0, 'fwd_fee': 733339, 'created_lt': 39515024000002, 'created_at': 1690098531}, 'init': None, 'body': 0 refs>}, 'out_msgs': None, 'total_fees': {'grams': 91, 'other': None}, 'state_update': {'old_hash': b'\x03}\xbb\x93v\x00\xa7\xc7w\x96\x07\x14\x9a\xafo\xb0\xc3\x16n)\xc4\x02i\x99s\xef\x1f\xf0\xb2\xb6\xd77', 'new_hash': b'\xd6U\xcc\x0bD\xf2\xaeh\xcf\xe8\xb7I\xa8v\x1a9\xee\x87\x12\xa0\xdb#\xe4\xc3\xa5\xf3\xf5\xae&\x01\xc7\xa7'}, 'description': {'type_': 'ordinary', 'credit_first': False, 'storage_ph': {'storage_fees_collected': 91, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': {'due_fees_collected': None, 'credit': {'grams': 1000, 'other': None}}, 'compute_ph': {'type_': 'skipped', 'reason': {'type_': 'no_gas'}}, 'action': None, 'aborted': True, 'bounce': {'type_': 'nofunds', 'msg_size': {'cells': None, 'bits': None}, 'req_fwd_fees': 1000000}, 'destroyed': False}}, {'account_addr': '6f5bc67986e06430961d9df00433926a4cd92e597ddd8aa6043645ac20bd1782', 'lt': 39514911000001, 'prev_trans_hash': b'\xdc#q\x05\x98\x7f\x90\xa8\xde\x85\xf5\xab\x0bqB\xb9\xae\xb1\xe9:\x8b;t\xf4\\\xa1\xda\xc1V\xa1B\xa7', 'prev_trans_lt': 39496961000001, 'now': 1690098174, 'outmsg_cnt': 1, 'orig_status': {'type_': 'active'}, 'end_status': {'type_': 'active'}, 'in_msg': {'info': {'src': None, 'dest': Address, 'import_fee': 0}, 'init': None, 'body': 1 refs>}, 'out_msgs': [{'info': {'ihr_disabled': True, 'bounce': False, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 45840000, 'other': None}, 'value_coins': 45840000, 'ihr_fee': 0, 'fwd_fee': 2773355, 'created_lt': 39514911000002, 'created_at': 1690098174}, 'init': {'split_depth': None, 'special': None, 'code': 1 refs>, 'data': 0 refs>, 'library': None}, 'body': 0 refs>}], 'total_fees': {'grams': 10134337, 'other': None}, 'state_update': {'old_hash': b'\xfe\xb4\x8a\x18!\xb4\x82\x8cg\x8aXfkI|\xcb\xa9i\xcaEh\x9c%q\x14M\xd3\xe5\xbf\xb2\xe0E', 'new_hash': b'\x03}\xbb\x93v\x00\xa7\xc7w\x96\x07\x14\x9a\xafo\xb0\xc3\x16n)\xc4\x02i\x99s\xef\x1f\xf0\xb2\xb6\xd77'}, 'description': {'type_': 'ordinary', 'credit_first': True, 'storage_ph': {'storage_fees_collected': 14692, 'storage_fees_due': None, 'status_change': {'type_': 'unchanged'}}, 'credit_ph': None, 'compute_ph': {'type_': 'vm', 'success': True, 'msg_state_used': False, 'account_activated': False, 'gas_fees': 3308000, 'gas_used': 3308, 'gas_limit': None, 'gas_credit': 10000, 'mode': 0, 'exit_code': 0, 'exit_arg': None, 'vm_steps': 68, 'vm_init_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'vm_final_state_hash': b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'}, 'action': {'success': True, 'valid': True, 'no_funds': False, 'status_change': {'type_': 'unchanged'}, 'total_fwd_fees': 4160000, 'total_action_fees': 1386645, 'result_code': 0, 'result_arg': None, 'tot_actions': 1, 'spec_actions': 0, 'skipped_actions': 0, 'msgs_created': 1, 'action_list_hash': b'd\xeb\xc4\x9ax*\xe5\x8ds\x03\xd6p\xd1\xb5Z\xd6s{\x8da\x81\x8e\x9b$\x91\x96[yLg\xe6&', 'tot_msg_size': {'cells': 13, 'bits': 2666}}, 'aborted': False, 'bounce': None, 'destroyed': False}}] 11 | 12 | transaction = trs[2] 13 | print(transaction.in_msg.info) # {'ihr_disabled': True, 'bounce': True, 'bounced': False, 'src': Address, 'dest': Address, 'value': {'grams': 1000, 'other': None}, 'value_coins': 1000, 'ihr_fee': 0, 'fwd_fee': 733339, 'created_lt': 39515024000003, 'created_at': 1690098531} 14 | body = transaction.in_msg.body 15 | print([body.begin_parse()]) # [ 1 refs>] 16 | 17 | 18 | if __name__ == '__main__': 19 | asyncio.run(main()) 20 | -------------------------------------------------------------------------------- /examples/wallets/highload.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from pytoniq import LiteBalancer 3 | from pytoniq_core import Cell 4 | from pytoniq.contract.wallets.highload_v3 import HighloadWalletV3 5 | 6 | 7 | async def main(): 8 | async with LiteBalancer.from_mainnet_config(trust_level=2) as client: 9 | mnemo = [] 10 | wallet = await HighloadWalletV3.from_mnemonic(client, mnemo) 11 | 12 | # deploy wallet if needed 13 | await wallet.deploy_via_external() 14 | 15 | # send 1000 messages to zero address, each message has 3 ton attached and empty body 16 | await wallet.transfer(destinations=['EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'] * 1000, amounts=[3 * 10**9] * 1000, bodies=[Cell.empty()] * 1000) 17 | 18 | 19 | if __name__ == '__main__': 20 | asyncio.run(main()) 21 | -------------------------------------------------------------------------------- /pytoniq/__init__.py: -------------------------------------------------------------------------------- 1 | from .contract import * 2 | from .liteclient import * 3 | from .adnl import * 4 | from pytoniq_core.boc import * 5 | from pytoniq_core.crypto import * 6 | from pytoniq_core.proof import * 7 | from pytoniq_core.tl import * 8 | from pytoniq_core.tlb import * 9 | -------------------------------------------------------------------------------- /pytoniq/adnl/__init__.py: -------------------------------------------------------------------------------- 1 | from .adnl import AdnlTransport, AdnlTransportError, AdnlChannel, Node 2 | from .dht import DhtNode, DhtError, DhtClient, DhtValueNotFoundError 3 | from .overlay import OverlayTransport, OverlayNode, OverlayTransportError 4 | -------------------------------------------------------------------------------- /pytoniq/adnl/adnl.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | import inspect 4 | import logging 5 | import random 6 | import time 7 | import hashlib 8 | import typing 9 | from asyncio import transports 10 | from typing import Any 11 | 12 | from pytoniq_core.tl.generator import TlGenerator 13 | 14 | from pytoniq_core.crypto.ciphers import Server, Client, AdnlChannel, get_random, aes_ctr_encrypt, aes_ctr_decrypt, get_shared_key, create_aes_ctr_sipher_from_key_n_data 15 | 16 | 17 | class SocketProtocol(asyncio.DatagramProtocol): 18 | 19 | def __init__(self, timeout: int = 10): 20 | # https://github.com/eerimoq/asyncudp/blob/main/asyncudp/__init__.py 21 | self._error = None 22 | self._packets = asyncio.Queue(10000) 23 | self.timeout = timeout 24 | self.logger = logging.getLogger(self.__class__.__name__) 25 | 26 | def connection_made(self, transport: transports.DatagramTransport) -> None: 27 | super().connection_made(transport) 28 | 29 | def datagram_received(self, data: bytes, addr: typing.Tuple[typing.Union[str, Any], int]) -> None: 30 | self.logger.debug(f'received {len(data)} bytes') 31 | self._packets.put_nowait((data, addr)) 32 | super().datagram_received(data, addr) 33 | 34 | def error_received(self, exc: Exception) -> None: 35 | raise exc 36 | super().error_received(exc) 37 | 38 | async def receive(self): 39 | return await self._packets.get() 40 | 41 | 42 | class Node(Server): 43 | 44 | def __init__( 45 | self, 46 | peer_host: str, # ipv4 host 47 | peer_port: int, # port 48 | peer_pub_key: str, 49 | transport: "AdnlTransport" 50 | ): 51 | self.host = peer_host 52 | self.port = peer_port 53 | super().__init__(peer_host, peer_port, base64.b64decode(peer_pub_key)) 54 | self.channels: typing.List[AdnlChannel] = [] 55 | self.seqno = 1 56 | self.confirm_seqno = 0 57 | self.key_id = self.get_key_id() 58 | self.transport = transport 59 | self.pinger: asyncio.Task = None 60 | self.connected = False 61 | self.logger = logging.getLogger(self.__class__.__name__) 62 | self._lost_pings = 0 63 | 64 | async def connect(self): 65 | return await self.transport.connect_to_peer(self) 66 | 67 | async def send_ping(self) -> None: 68 | random_id = get_random(8) 69 | resp = await self.transport.send_query_message(tl_schema_name='dht.ping', data={'random_id': random_id}, peer=self) 70 | assert resp[0].get('random_id') == int.from_bytes(random_id, 'big', signed=True) 71 | 72 | def start_ping(self): 73 | self.pinger = asyncio.create_task(self.ping()) 74 | 75 | async def ping(self): 76 | while True: 77 | try: 78 | await self.send_ping() 79 | self._lost_pings = 0 80 | self.logger.debug(f'pinged {self.key_id.hex()}') 81 | except asyncio.TimeoutError: 82 | self._lost_pings += 1 83 | if self._lost_pings > 3: 84 | if self.key_id in self.transport.peers: 85 | self.transport.peers.pop(self.key_id) 86 | await self.disconnect() 87 | await asyncio.sleep(5) 88 | 89 | async def get_signed_address_list(self): 90 | return (await self.transport.send_query_message('dht.getSignedAddressList', {}, self))[0] 91 | 92 | @property 93 | def addr(self) -> typing.Tuple[str, int]: 94 | """ 95 | :return: ipv4 node address as (host, port) 96 | """ 97 | return self.host, self.port 98 | 99 | def inc_seqno(self): 100 | self.seqno += 1 101 | 102 | async def disconnect(self): 103 | if self.connected: 104 | self.connected = False 105 | self.pinger.cancel() 106 | 107 | 108 | class AdnlTransportError(Exception): 109 | pass 110 | 111 | 112 | class AdnlTransport: 113 | 114 | def __init__(self, 115 | private_key: bytes = None, 116 | tl_schemas_path: str = None, 117 | local_address: tuple = ('0.0.0.0', None), 118 | *args, **kwargs 119 | ) -> None: 120 | """ 121 | ADNL Transport abstract class 122 | """ 123 | 124 | """########### init ###########""" 125 | self.loop: asyncio.AbstractEventLoop = None 126 | self.timeout = kwargs.get('timeout', 10) 127 | self.listener: asyncio.Task = None 128 | self.logger = logging.getLogger(self.__class__.__name__) 129 | self.tasks: typing.Dict[str, asyncio.Future] = {} 130 | self.query_handlers: typing.Dict[str, typing.Callable] = {} 131 | self.custom_handlers: typing.Dict[str, typing.Callable] = {} 132 | self._message_parts: typing.Dict[str, dict] = {} # {'hash': {'remained': int, 'parts': list}} 133 | 134 | """########### connection ###########""" 135 | self.transport: asyncio.DatagramTransport = None 136 | self.protocol: SocketProtocol = None 137 | if local_address[1] is None: 138 | local_address = local_address[0], random.randint(10000, 60000) 139 | self.local_address = local_address 140 | 141 | """########### TL ###########""" 142 | if tl_schemas_path is None: 143 | self.schemas = TlGenerator.with_default_schemas().generate() 144 | else: 145 | self.schemas = TlGenerator(tl_schemas_path).generate() 146 | self.adnl_query_sch = self.schemas.get_by_name('adnl.message.query') 147 | self.adnl_packet_content_sch = self.schemas.get_by_name('adnl.packetContents') 148 | self.create_channel_sch = self.schemas.get_by_name('adnl.message.createChannel') 149 | 150 | """########### crypto ###########""" 151 | self.peers: typing.Dict[bytes, Node] = {} 152 | if private_key is None: 153 | private_key = Client.generate_ed25519_private_key() 154 | self.client = Client(private_key) 155 | self.local_id = self.client.get_key_id() 156 | self.channels: typing.Dict[bytes, AdnlChannel] = {} 157 | self.enc_sipher = None 158 | self.dec_sipher = None 159 | 160 | @staticmethod 161 | def _get_rand(): 162 | rand = get_random(16) 163 | if rand[0] & 1 > 0: 164 | return rand[1:] 165 | return rand[1:8] 166 | 167 | @staticmethod 168 | def compute_flags_for_packet(data: dict) -> dict: 169 | """ 170 | :param data: dict with TL Scheme arguments 171 | :return: data with computed flags field 172 | """ 173 | flags = 0 174 | if 'from' in data: 175 | flags += 1 << 0 176 | if 'from_short' in data: 177 | flags += 1 << 1 178 | if 'message' in data: 179 | flags += 1 << 2 180 | if 'messages' in data: 181 | flags += 1 << 3 182 | if 'address' in data: 183 | flags += 1 << 4 184 | if 'priority_address' in data: 185 | flags += 1 << 5 186 | if 'seqno' in data: 187 | flags += 1 << 6 188 | if 'confirm_seqno' in data: 189 | flags += 1 << 7 190 | if 'recv_addr_list_version' in data: 191 | flags += 1 << 8 192 | if 'recv_priority_addr_list_version' in data: 193 | flags += 1 << 9 194 | if 'reinit_date' in data or 'dst_reinit_date' in data: 195 | flags += 1 << 10 196 | if 'signature' in data: 197 | flags += 1 << 11 198 | 199 | return data | {'flags': flags} 200 | 201 | def _prepare_packet_content_msg(self, data: dict, peer: Node = None) -> dict: 202 | """ 203 | Adds random bytes, seqno, confirm_seqno and flags in message args if they were not provided 204 | """ 205 | if 'rand1' not in data or 'rand2' not in data: 206 | data['rand1'] = self._get_rand() 207 | data['rand2'] = self._get_rand() 208 | 209 | if data.get('seqno') is None: 210 | if peer is None: 211 | raise AdnlTransportError('Must either specify seqno in data or provide peer to method') 212 | data['seqno'] = peer.seqno 213 | if data.get('confirm_seqno') is None: 214 | if peer is None: 215 | raise AdnlTransportError('Must either specify confirm_seqno in data or provide peer to method') 216 | data['confirm_seqno'] = peer.confirm_seqno 217 | 218 | return self.compute_flags_for_packet(data) 219 | 220 | def _decrypt_any(self, resp_packet: bytes) -> typing.Tuple[bytes, typing.Optional[Node]]: 221 | """ 222 | :param resp_packet: bytes of received packet 223 | :return: decrypted packet and maybe `Node` 224 | """ 225 | key_id = resp_packet[:32] 226 | if key_id == self.client.get_key_id(): 227 | server_public_key = resp_packet[32:64] 228 | checksum = resp_packet[64:96] 229 | encrypted = resp_packet[96:] 230 | 231 | peer_crypto = Server('', 0, server_public_key) 232 | 233 | shared_key = get_shared_key(self.client.x25519_private.encode(), 234 | peer_crypto.x25519_public.encode()) 235 | 236 | dec_cipher = create_aes_ctr_sipher_from_key_n_data(shared_key, checksum) 237 | decrypted = aes_ctr_decrypt(dec_cipher, encrypted) 238 | assert hashlib.sha256(decrypted).digest() == checksum, 'invalid checksum' 239 | return decrypted, None 240 | else: 241 | for peer_id, channel in self.channels.items(): 242 | if key_id == channel.server_aes_key_id: 243 | checksum = resp_packet[32:64] 244 | encrypted = resp_packet[64:] 245 | decrypted = channel.decrypt(encrypted, checksum) 246 | assert hashlib.sha256(decrypted).digest() == checksum, 'invalid checksum' 247 | return decrypted, self.peers.get(peer_id) 248 | # TODO make new connection 249 | self.logger.debug(f'unknown key id from node: {key_id.hex()}') 250 | return b'', None 251 | 252 | def _process_outcoming_message(self, message: dict) -> typing.Optional[asyncio.Future]: 253 | future = self.loop.create_future() 254 | type_ = message['@type'] 255 | if type_ == 'adnl.message.query': 256 | self.tasks[message.get('query_id')[::-1].hex()] = future 257 | elif type_ == 'adnl.message.createChannel': 258 | self.tasks[message.get('key')] = future 259 | else: 260 | return 261 | return future 262 | 263 | def _create_futures(self, data: dict) -> typing.List[asyncio.Future]: 264 | futures = [] 265 | if data.get('message'): 266 | future = self._process_outcoming_message(data['message']) 267 | if future is not None: 268 | futures.append(future) 269 | 270 | if data.get('messages'): 271 | for message in data['messages']: 272 | future = self._process_outcoming_message(message) 273 | if future is not None: 274 | futures.append(future) 275 | return futures 276 | 277 | @staticmethod 278 | async def _receive(futures: typing.List[asyncio.Future]) -> list: 279 | return list(await asyncio.gather(*futures)) 280 | 281 | async def _process_incoming_message(self, message: dict, peer: Node): 282 | if peer: 283 | self.logger.debug(f'Received message {message} from peer {peer.get_key_id().hex()}') 284 | if message['@type'] == 'adnl.message.answer': 285 | future = self.tasks.pop(message.get('query_id')) 286 | future.set_result(message['answer']) 287 | elif message['@type'] == 'adnl.message.confirmChannel': 288 | if message.get('peer_key') in self.tasks: 289 | future = self.tasks.pop(message.get('peer_key')) 290 | future.set_result(message) 291 | elif message['@type'] == 'adnl.message.query': 292 | if peer is None: 293 | self.logger.info(f'Received query message from unknown peer: {message}') 294 | # not implemented, todo: make connection with new peer 295 | return 296 | await self._process_query_message(message, peer) 297 | elif message['@type'] == 'adnl.message.custom': 298 | if peer is None: 299 | # should not ever happen fixme 300 | self.logger.info(f'Received custom message from unknown peer: {message}') 301 | return 302 | await self._process_custom_message(message, peer) 303 | elif message['@type'] == 'adnl.message.part': 304 | hash_ = message['hash'] 305 | if hash_ not in self._message_parts: 306 | self._message_parts[hash_] = {'remained': message['total_size'], 'parts': []} 307 | 308 | self._message_parts[hash_]['remained'] -= len(message['data']) 309 | self._message_parts[hash_]['parts'].append(message) 310 | 311 | if self._message_parts[hash_]['remained'] == 0: 312 | data = self._collect_adnl_message_parts(hash_) 313 | if isinstance(data, dict) and data['@type'] != 'adnl.message.part': # to avoid infinity recursion, but should never happen 314 | await self._process_incoming_message(data, peer) 315 | else: 316 | self.logger.info(f'unexpected message type received: {message}') 317 | # raise AdnlTransportError(f'unexpected message type received: {message}') 318 | 319 | def _collect_adnl_message_parts(self, hash_: str, deserialize_after: bool = True): 320 | if hash_ not in self._message_parts: 321 | raise AdnlTransportError(f'Provided hash not in message parts') 322 | parts = sorted(self._message_parts[hash_]['parts'], key=lambda i: i['offset']) 323 | full_data = b'' 324 | for part in parts: 325 | full_data += part['data'] 326 | if deserialize_after: 327 | return self.schemas.deserialize(full_data)[0] 328 | 329 | async def _process_query_message(self, message: dict, peer: Node): 330 | query = message.get('query') 331 | # it's divided into separate method because some higher level protocols over ADNL need specific query processing 332 | await self._process_query_handler(message, query, peer) 333 | 334 | async def _process_query_handler(self, message: dict, query: dict, peer: Node): 335 | # try to get handler for specific query and if there is no, try to get default handler 336 | handler = self.query_handlers.get(query['@type'], self.query_handlers.get(None)) 337 | if handler: 338 | if inspect.iscoroutinefunction(handler): 339 | response = await handler(query) 340 | else: 341 | response = handler(query) 342 | if response is not None: 343 | await self.send_answer_message(response, message.get('query_id'), peer) 344 | 345 | async def _process_custom_message(self, message: dict, peer: Node): 346 | data = message.get('data') 347 | await self._process_custom_message_handler(data, peer) 348 | 349 | async def _process_custom_message_handler(self, data: dict, peer: Node): 350 | handler = self.custom_handlers.get(data['@type'], self.custom_handlers.get(None)) 351 | if handler: 352 | if inspect.iscoroutinefunction(handler): 353 | response = await handler(data) 354 | else: 355 | response = handler(data) 356 | if response is not None: 357 | await self.send_custom_message(response, peer) 358 | 359 | def set_query_handler(self, type_: str, handler: typing.Callable) -> None: 360 | """ 361 | :param type_: TL type of message 362 | :param handler: function to handle message. **Must** return dict or bytes or None. If 363 | None returned than answer won't be sent to the sender 364 | :return: 365 | """ 366 | self.query_handlers[type_] = handler 367 | 368 | def set_default_query_handler(self, handler: typing.Callable): 369 | """ 370 | Same as `set_query_handler` when there is no handlers for query specific type. 371 | :param handler: 372 | :return: 373 | """ 374 | self.set_query_handler(None, handler) 375 | 376 | def set_custom_message_handler(self, type_: str, handler: typing.Callable): 377 | """ 378 | :param type_: TL type of message 379 | :param handler: function to handle message. **Must** return dict or bytes or None. If 380 | None returned than answer won't be sent to the sender. 381 | :return: 382 | """ 383 | self.custom_handlers[type_] = handler 384 | 385 | def set_default_custom_message_handler(self, handler: typing.Callable): 386 | """ 387 | Same as `set_custom_message_handler` when there is no handlers for query specific type. 388 | :param handler: 389 | :return: 390 | """ 391 | self.set_custom_message_handler(None, handler) 392 | 393 | async def listen(self): 394 | while True: 395 | packet, addr = await self.protocol.receive() 396 | 397 | decrypted, peer = self._decrypt_any(packet) 398 | if not decrypted: 399 | continue 400 | response = self.schemas.deserialize(decrypted)[0] 401 | 402 | if peer is None: 403 | if 'from_short' in response: 404 | peer = self.peers.get(bytes.fromhex(response['from_short']['id'])) 405 | 406 | if peer is not None: 407 | received_seqno = response.get('seqno', 0) 408 | if received_seqno > peer.confirm_seqno: 409 | peer.confirm_seqno = received_seqno 410 | 411 | message = response.get('message') 412 | messages = response.get('messages') 413 | 414 | if message: 415 | await self._process_incoming_message(message, peer) 416 | if messages: 417 | for message in messages: 418 | await self._process_incoming_message(message, peer) 419 | 420 | async def send_message_in_channel(self, data: dict, channel: typing.Optional[AdnlChannel] = None, peer: Node = None) -> list: 421 | 422 | if peer is None: 423 | raise AdnlTransportError('Must provide peer') 424 | 425 | data = self._prepare_packet_content_msg(data, peer) 426 | sending_seqno = data.get('seqno') 427 | 428 | futures = self._create_futures(data) 429 | 430 | if channel is None: 431 | if not len(peer.channels): 432 | raise AdnlTransportError(f'Peer has no channels and channel was not provided') 433 | channel = peer.channels[0] 434 | 435 | if peer.seqno == sending_seqno: 436 | peer.inc_seqno() 437 | else: 438 | raise Exception(f'sending seqno {sending_seqno}, client seqno: {peer.seqno}') 439 | serialized = self.schemas.serialize(self.adnl_packet_content_sch, data) 440 | res = channel.encrypt(serialized) 441 | 442 | self.transport.sendto(res, addr=peer.addr) 443 | result = await asyncio.wait_for(self._receive(futures), self.timeout) 444 | 445 | return result 446 | 447 | async def send_message_outside_channel(self, data: dict, peer: Node) -> list: 448 | """ 449 | Serializes, signs and encrypts sending message. 450 | :param peer: peer 451 | :param data: data for `adnl.packetContents` TL Scheme 452 | :return: decrypted and deserialized response 453 | """ 454 | data = self._prepare_packet_content_msg(data, peer) 455 | sending_seqno = data.get('seqno') 456 | 457 | data = self.compute_flags_for_packet(data) 458 | 459 | futures = self._create_futures(data) 460 | 461 | serialized1 = self.schemas.serialize(self.adnl_packet_content_sch, self.compute_flags_for_packet(data)) 462 | signature = self.client.sign(serialized1) 463 | serialized2 = self.schemas.serialize(self.adnl_packet_content_sch, 464 | self.compute_flags_for_packet(data | {'signature': signature})) 465 | 466 | checksum = hashlib.sha256(serialized2).digest() 467 | shared_key = get_shared_key(self.client.x25519_private.encode(), peer.x25519_public.encode()) 468 | init_cipher = create_aes_ctr_sipher_from_key_n_data(shared_key, checksum) 469 | data = aes_ctr_encrypt(init_cipher, serialized2) 470 | 471 | res = peer.get_key_id() + self.client.ed25519_public.encode() + checksum + data 472 | self.transport.sendto(res, addr=(peer.host, peer.port)) 473 | 474 | if peer.seqno == sending_seqno: 475 | peer.inc_seqno() 476 | else: 477 | raise Exception(f'sending seqno {sending_seqno}, client seqno: {peer.seqno}') 478 | if futures: 479 | result = await asyncio.wait_for(self._receive(futures), self.timeout) 480 | return result 481 | 482 | async def start(self): 483 | self.loop = asyncio.get_running_loop() 484 | self.transport, self.protocol = await self.loop.create_datagram_endpoint( 485 | lambda: SocketProtocol(timeout=self.timeout), 486 | local_addr=self.local_address, 487 | reuse_port=True 488 | ) 489 | self.listener = self.loop.create_task(self.listen()) 490 | return 491 | 492 | def _get_default_message(self): 493 | return { 494 | '@type': 'adnl.message.query', 495 | 'query_id': get_random(32), 496 | 'query': self.schemas.get_by_name('dht.getSignedAddressList').little_id() 497 | } 498 | 499 | async def connect_to_peer(self, peer: Node) -> list: 500 | """ 501 | Connects to the peer, creates channel and asks for a signed list in channel. 502 | :param peer: peer connect to 503 | :return: response dict for default message 504 | """ 505 | 506 | ts = int(time.time()) 507 | channel_client = Client(Client.generate_ed25519_private_key()) 508 | create_channel_message = { 509 | '@type': 'adnl.message.createChannel', 510 | 'key': channel_client.ed25519_public.encode().hex(), 511 | 'date': ts 512 | } 513 | 514 | default_message = self._get_default_message() 515 | 516 | from_ = self.schemas.serialize(self.schemas.get_by_name('pub.ed25519'), data={'key': self.client.ed25519_public.encode().hex()}) 517 | data = { 518 | 'from': from_, 519 | # 'from_short': {'id': self.client.get_key_id().hex()}, 520 | 'messages': [create_channel_message, default_message], 521 | 'address': { 522 | 'addrs': [], 523 | 'version': ts, 524 | 'reinit_date': ts, 525 | 'priority': 0, 526 | 'expire_at': 0, 527 | }, 528 | 'recv_addr_list_version': ts, 529 | 'reinit_date': ts, 530 | 'dst_reinit_date': 0, 531 | } 532 | 533 | messages = await self.send_message_outside_channel(data, peer) 534 | confirm_channel = messages[0] 535 | assert confirm_channel.get('@type') == 'adnl.message.confirmChannel', (f'expected adnl.message.confirmChannel,' 536 | f' got {confirm_channel.get("@type")}') 537 | assert confirm_channel['peer_key'] == channel_client.ed25519_public.encode().hex() 538 | 539 | channel_peer = Server(peer.host, peer.port, bytes.fromhex(confirm_channel['key'])) 540 | channel = AdnlChannel(channel_client, channel_peer, self.local_id, peer.get_key_id()) 541 | self.channels[peer.get_key_id()] = channel 542 | peer.channels.append(channel) 543 | 544 | peer.start_ping() 545 | peer.connected = True 546 | self.peers[peer.key_id] = peer 547 | 548 | return messages[1] 549 | 550 | async def close(self): 551 | self.listener.cancel() 552 | while not self.listener.cancelled(): 553 | await asyncio.sleep(0) 554 | self.transport.abort() 555 | 556 | async def send_query_message(self, tl_schema_name: str, data: dict, peer: Node) -> typing.List[dict]: 557 | message = { 558 | '@type': 'adnl.message.query', 559 | 'query_id': get_random(32), 560 | 'query': self.schemas.serialize( 561 | self.schemas.get_by_name(tl_schema_name), 562 | data 563 | ) 564 | } 565 | 566 | data = { 567 | 'message': message, 568 | } 569 | 570 | result = await self.send_message_in_channel(data, None, peer) 571 | return result 572 | 573 | async def send_answer_message(self, response: typing.Union[dict, bytes], query_id: bytes, peer: Node): 574 | message = { 575 | '@type': 'adnl.message.answer', 576 | 'query_id': query_id, 577 | 'answer': response 578 | } 579 | 580 | data = { 581 | 'message': message, 582 | } 583 | return await self.send_message_in_channel(data, None, peer) 584 | 585 | async def send_custom_message(self, message: typing.Union[dict, bytes], peer: Node) -> list: 586 | 587 | custom_message = { 588 | '@type': 'adnl.message.custom', 589 | 'data': message 590 | } 591 | 592 | data = { 593 | 'message': custom_message, 594 | } 595 | 596 | result = await self.send_message_in_channel(data, None, peer) 597 | return result 598 | -------------------------------------------------------------------------------- /pytoniq/adnl/dht.py: -------------------------------------------------------------------------------- 1 | import asyncio.exceptions 2 | import base64 3 | import time 4 | import typing 5 | import hashlib 6 | import socket 7 | import struct 8 | 9 | import requests 10 | from pytoniq_core.crypto.ciphers import Client, Server 11 | from pytoniq_core.crypto.signature import verify_sign 12 | from pytoniq_core.tl import TlGenerator 13 | 14 | from .adnl import Node, AdnlTransport 15 | from .overlay import OverlayNode, OverlayTransport 16 | 17 | 18 | class DhtError(Exception): 19 | pass 20 | 21 | 22 | class DhtValueNotFoundError(DhtError): 23 | pass 24 | 25 | 26 | class DhtNode(Node): 27 | 28 | async def find_value(self, key: bytes, k: int = 6): 29 | data = {'key': key.hex(), 'k': k} 30 | return await self.transport.send_query_message('dht.findValue', data, self) 31 | 32 | async def store_value(self, value: dict): 33 | data = {'value': value} 34 | return await self.transport.send_query_message('dht.store', data, self) 35 | 36 | @classmethod 37 | def from_dict(cls, transport: AdnlTransport, data: dict, check_signature=True) -> "DhtNode": 38 | try: 39 | pub_k = bytes.fromhex(data['id']['key']) 40 | pub_k_b64 = base64.b64encode(pub_k) 41 | except ValueError: 42 | pub_k_b64 = data['id']['key'] 43 | pub_k = base64.b64decode(pub_k_b64) 44 | data['id']['key'] = pub_k.hex() 45 | if isinstance(data['signature'], bytes): 46 | signature = data['signature'] 47 | else: 48 | signature = base64.b64decode(data['signature']) 49 | data['signature'] = b'' 50 | 51 | # check signature 52 | if check_signature: 53 | schemas = TlGenerator.with_default_schemas().generate() 54 | signed_message = schemas.serialize(schema=schemas.get_by_name('dht.node'), data=data) 55 | if not verify_sign(pub_k, signed_message, signature): 56 | raise Exception('invalid node signature!') 57 | 58 | node_addr = data['addr_list']['addrs'][0] 59 | host = socket.inet_ntoa(struct.pack('>i', node_addr['ip'])) 60 | return cls(peer_host=host, peer_port=node_addr['port'], peer_pub_key=pub_k_b64, transport=transport) 61 | 62 | 63 | class DhtClient: 64 | 65 | def __init__(self, 66 | nodes: typing.List[DhtNode], 67 | adnl_transport: AdnlTransport, 68 | tl_schemas_path: typing.Optional[str] = None 69 | ): 70 | self.adnl_transport: AdnlTransport = adnl_transport 71 | self.nodes_set: set = set(nodes) 72 | assert len(nodes) >= 1, 'expected at least 1 node in the list' 73 | if tl_schemas_path is None: 74 | self.schemas = TlGenerator.with_default_schemas().generate() 75 | else: 76 | self.schemas = TlGenerator(tl_schemas_path).generate() 77 | 78 | async def close(self): 79 | """ 80 | disconnects to all known nodes 81 | :return: 82 | """ 83 | for node in self.nodes_set: 84 | await node.disconnect() 85 | 86 | def get_dht_key_id_tl(self, id_: typing.Union[bytes, str], name: bytes = b'address', idx: int = 0): 87 | if isinstance(id_, str): 88 | id_ = bytes.fromhex(id_) 89 | dht_key_sch = self.schemas.get_by_name('dht.key') 90 | serialized = self.schemas.serialize(dht_key_sch, data={'id': id_.hex(), 'name': name, 'idx': idx}) 91 | return hashlib.sha256(serialized).digest() 92 | 93 | @staticmethod 94 | def get_dht_key_id(id_: typing.Union[bytes, str], name: bytes = b'address', idx: int = 0): 95 | """ 96 | Same as the method above but without using TlGenerator 97 | """ 98 | if isinstance(id_, str): 99 | id_ = bytes.fromhex(id_) 100 | to_hash = b'\x8f\xdeg\xf6' + id_ + len(name).to_bytes(1, 'big') + name + idx.to_bytes(4, 'little') 101 | return hashlib.sha256(to_hash).digest() 102 | 103 | @staticmethod 104 | def find_distance_between_nodes(key_id_1: typing.Union[bytes, int], key_id_2: typing.Union[bytes, int]): 105 | if isinstance(key_id_1, bytes): 106 | key_id_1 = int.from_bytes(key_id_1, 'big') 107 | if isinstance(key_id_2, bytes): 108 | key_id_2 = int.from_bytes(key_id_2, 'big') 109 | return key_id_1 ^ key_id_2 110 | 111 | @classmethod 112 | def build_priority_list(cls, nodes: typing.Iterable, key_id: bytes) -> typing.List[DhtNode]: 113 | return sorted(nodes, key=lambda i: cls.find_distance_between_nodes(i.key_id, key_id)) 114 | 115 | async def find_value(self, key: bytes, k: int = 6, timeout: int = 10): 116 | start_time = time.time() 117 | while True: 118 | if time.time() - start_time > timeout: # TODO improve timeout 119 | raise asyncio.exceptions.TimeoutError() 120 | nodes = self.build_priority_list(self.nodes_set, key) 121 | 122 | for node in nodes: 123 | if not node.connected: 124 | try: 125 | await asyncio.wait_for(node.connect(), 1) 126 | except asyncio.TimeoutError: 127 | continue 128 | try: 129 | resp = await node.find_value(key=key, k=k) 130 | except asyncio.exceptions.TimeoutError: 131 | continue 132 | except Exception as e: 133 | raise e # ? 134 | 135 | resp = resp[0] 136 | if resp['@type'] == 'dht.valueNotFound': 137 | new_nodes = resp['nodes']['nodes'] 138 | new_nodes_set = set() 139 | for n in new_nodes: 140 | new_nodes_set.add(DhtNode.from_dict(self.adnl_transport, n, True)) 141 | old_nodes = self.nodes_set.copy() 142 | self.nodes_set = self.nodes_set.union(new_nodes_set) 143 | if self.nodes_set == old_nodes: 144 | raise DhtValueNotFoundError(f'value {key.hex()} not found') 145 | break 146 | elif resp['@type'] == 'dht.valueFound': 147 | return resp 148 | else: 149 | raise DhtError(f'received unknown response type: {resp}') 150 | 151 | async def raw_store_value(self, dht_value: dict, try_find_after: bool = True): 152 | """ 153 | dht.store value:dht.value = dht.Stored; 154 | 155 | :param dht_value: dict that represents `dht.value` TL scheme 156 | :param try_find_after: tries to find value in known peers after storage 157 | :return: bool was value stored or not 158 | """ 159 | s = time.time() 160 | key = dht_value.get('key', {}).get('key', {}) 161 | if key is None: 162 | raise DhtError(f'must provide dht.value dict to the method') 163 | key_id = self.get_dht_key_id(bytes.fromhex(key.get('id')), key.get('name'), key.get('idx')) 164 | nodes = self.build_priority_list(self.nodes_set, key_id) 165 | stored = False 166 | for node in nodes: 167 | if not node.connected: 168 | try: 169 | await asyncio.wait_for(node.connect(), 2) 170 | except asyncio.TimeoutError: 171 | continue 172 | try: 173 | resp = await node.store_value(dht_value) 174 | assert resp[0]['@type'] == 'dht.stored' 175 | stored = True 176 | except asyncio.exceptions.TimeoutError: 177 | continue 178 | except Exception as e: 179 | raise e # ? 180 | stored = True 181 | 182 | if try_find_after: 183 | try: 184 | await self.find_value(key_id, timeout=10) 185 | except asyncio.exceptions.TimeoutError: 186 | return False 187 | except DhtValueNotFoundError: 188 | return False 189 | 190 | return stored 191 | 192 | @staticmethod 193 | def get_dht_key(id_: bytes, name: bytes = b'address', idx: int = 0): 194 | return {'id': id_.hex(), 'name': name, 'idx': idx} 195 | 196 | async def store_value(self, key: dict, value: bytes, private_key: bytes, 197 | update_rule: typing.Literal['signature', 'anybody', 'overlayNodes'] = 'signature', 198 | ttl: int = 30, try_find_after: bool = True): 199 | if update_rule != 'signature': 200 | raise DhtError('currently overlay is not supported') 201 | pk = Client(ed25519_private_key=private_key) 202 | key_description = { 203 | 'key': key, 204 | 'id': { 205 | '@type': 'pub.ed25519', 206 | 'key': pk.ed25519_public.encode().hex() 207 | }, 208 | 'update_rule': self.schemas.get_by_name('dht.updateRule.' + update_rule).little_id(), 209 | 'signature': b'' 210 | } 211 | signature = pk.sign(self.schemas.serialize(self.schemas.get_by_name('dht.keyDescription'), key_description)) 212 | 213 | data = { 214 | 'key': key_description | {'signature': signature}, 215 | 'value': value, 216 | 'ttl': int(time.time()) + ttl, 217 | 'signature': b'' 218 | } 219 | signature = pk.sign(self.schemas.serialize(self.schemas.get_by_name('dht.value'), data)) 220 | 221 | data |= {'signature': signature} 222 | return await self.raw_store_value(data, try_find_after) 223 | 224 | async def get_overlay_nodes(self, overlay_id: typing.Union[bytes, str], overlay_transport: OverlayTransport): 225 | resp = await self.find_value(key=self.get_dht_key_id_tl(overlay_id, name=b'nodes'), timeout=30) 226 | nodes = resp['value']['value']['nodes'] 227 | result = [] 228 | for node in nodes: 229 | result.append(await self.get_overlay_node(node, overlay_transport)) 230 | return result 231 | 232 | async def get_overlay_node(self, node: dict, overlay_transport: OverlayTransport) -> typing.Optional[OverlayNode]: 233 | """ 234 | :param node: dict overlay.node TL schema 235 | :param overlay_transport: 236 | :return: OverlayNode or None 237 | """ 238 | pub_k = bytes.fromhex(node['id']['key']) 239 | adnl_addr = Server('', 0, pub_key=pub_k).get_key_id() 240 | 241 | to_sign = self.schemas.serialize( 242 | schema=self.schemas.get_by_name('overlay.node.toSign'), 243 | data={'id': {'id': adnl_addr.hex()}, 'overlay': node['overlay'], 'version': node['version']} 244 | ) 245 | 246 | if not verify_sign(pub_k, to_sign, node['signature']): 247 | raise Exception('invalid node signature!') 248 | 249 | try: 250 | resp = await self.find_value(key=self.get_dht_key_id_tl(id_=adnl_addr), timeout=5) 251 | except asyncio.TimeoutError: 252 | return None 253 | 254 | node_addr = resp['value']['value']['addrs'][0] 255 | host = socket.inet_ntoa(struct.pack('>i', node_addr['ip'])) 256 | port = node_addr['port'] 257 | pub_k = base64.b64encode(bytes.fromhex(resp['value']['key']['id']['key'])).decode() 258 | 259 | node = OverlayNode(peer_host=host, peer_port=port, peer_pub_key=pub_k, transport=overlay_transport) 260 | return node 261 | 262 | @classmethod 263 | def from_config(cls, config: dict, adnl_transport: AdnlTransport): 264 | nodes = [] 265 | nodes_dict = config['dht']['static_nodes']['nodes'] 266 | for node in nodes_dict: 267 | nodes.append(DhtNode.from_dict(adnl_transport, node)) 268 | return cls(nodes, adnl_transport) 269 | 270 | @classmethod 271 | def from_mainnet_config(cls, adnl_transport: AdnlTransport): 272 | config = requests.get('https://ton.org/global-config.json').json() 273 | return cls.from_config(config, adnl_transport) 274 | 275 | @classmethod 276 | def from_testnet_config(cls, adnl_transport: AdnlTransport): 277 | config = requests.get('https://ton.org/testnet-global.config.json').json() 278 | return cls.from_config(config, adnl_transport) 279 | -------------------------------------------------------------------------------- /pytoniq/adnl/overlay.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import random 3 | import time 4 | import hashlib 5 | import typing 6 | 7 | from pytoniq_core.tl.generator import TlGenerator 8 | 9 | from pytoniq_core import BlockIdExt, Block, Slice 10 | from pytoniq_core.crypto.ciphers import get_random 11 | 12 | from .adnl import Node, AdnlTransport, AdnlTransportError 13 | 14 | 15 | class OverlayTransportError(AdnlTransportError): 16 | pass 17 | 18 | 19 | class OverlayNode(Node): 20 | 21 | def __init__( 22 | self, 23 | peer_host: str, # ipv4 host 24 | peer_port: int, # port 25 | peer_pub_key: str, 26 | transport: "OverlayTransport" 27 | ): 28 | self.transport: "OverlayTransport" = None 29 | super().__init__(peer_host, peer_port, peer_pub_key, transport) 30 | 31 | async def send_ping(self) -> None: 32 | peers = [ 33 | self.transport.get_signed_myself() 34 | ] 35 | await self.transport.send_query_message('overlay.getRandomPeers', {'peers': {'nodes': peers}}, peer=self) 36 | 37 | 38 | class OverlayTransport(AdnlTransport): 39 | 40 | def __init__(self, 41 | private_key: bytes = None, 42 | tl_schemas_path: str = None, 43 | local_address: tuple = ('0.0.0.0', None), 44 | overlay_id: typing.Union[str, bytes] = None, 45 | *args, **kwargs 46 | ) -> None: 47 | 48 | super().__init__(private_key, tl_schemas_path, local_address, *args, **kwargs) 49 | if overlay_id is None: 50 | raise OverlayTransportError('must provide overlay id in OverlayTransport') 51 | 52 | if isinstance(overlay_id, bytes): 53 | overlay_id = overlay_id.hex() 54 | 55 | self.overlay_id = overlay_id 56 | 57 | @staticmethod 58 | def get_overlay_id(zero_state_file_hash: typing.Union[bytes, str], 59 | workchain: int = 0, shard: int = -9223372036854775808) -> str: 60 | 61 | if isinstance(zero_state_file_hash, bytes): 62 | zero_state_file_hash = zero_state_file_hash.hex() 63 | 64 | schemes = TlGenerator.with_default_schemas().generate() 65 | 66 | sch = schemes.get_by_name('tonNode.shardPublicOverlayId') 67 | data = { 68 | "workchain": workchain, 69 | "shard": shard, 70 | "zero_state_file_hash": zero_state_file_hash 71 | } 72 | 73 | key_id = hashlib.sha256(schemes.serialize(sch, data)).digest() 74 | 75 | sch = schemes.get_by_name('pub.overlay') 76 | data = { 77 | 'name': key_id 78 | } 79 | 80 | key_id = schemes.serialize(sch, data) 81 | 82 | return hashlib.sha256(key_id).digest().hex() 83 | 84 | @classmethod 85 | def get_mainnet_overlay_id(cls, workchain: int = 0, shard: int = -9223372036854775808) -> str: 86 | return cls.get_overlay_id('5e994fcf4d425c0a6ce6a792594b7173205f740a39cd56f537defd28b48a0f6e', workchain, shard) 87 | 88 | @classmethod 89 | def get_testnet_overlay_id(cls, workchain: int = 0, shard: int = -9223372036854775808) -> str: 90 | return cls.get_overlay_id('67e20ac184b9e039a62667acc3f9c00f90f359a76738233379efa47604980ce8', workchain, shard) 91 | 92 | async def _process_query_message(self, message: dict, peer: OverlayNode): 93 | query = message.get('query') 94 | if isinstance(query, list): 95 | if query[0]['@type'] == 'overlay.query': 96 | assert query[0]['overlay'] == self.overlay_id, 'Unknown overlay id received' 97 | query = query[-1] 98 | await self._process_query_handler(message, query, peer) 99 | 100 | async def _process_custom_message(self, message: dict, peer: Node): 101 | data = message.get('data') 102 | if isinstance(data, list): 103 | if data[0]['@type'] in ('overlay.query', 'overlay.message'): 104 | assert data[0]['overlay'] == self.overlay_id, 'Unknown overlay id received' 105 | data = data[-1] 106 | if data['@type'] == 'overlay.broadcast': 107 | # Force broadcast distributing for the network stability. Can be removed in the future. 108 | # Note that this is almost takes no time to do and will be done in the background. 109 | asyncio.create_task(self.distribute_broadcast(data, ignore_errors=True)) 110 | 111 | await self._process_custom_message_handler(data, peer) 112 | 113 | async def distribute_broadcast(self, message: dict, ignore_errors: bool = True): 114 | tasks = [] 115 | peers = random.choices(list(self.peers.items()), k=3) # https://github.com/ton-blockchain/ton/blob/e30049930a7372a3c1d28a1e59956af8eb489439/overlay/overlay-broadcast.cpp#L69 116 | for _, peer in peers: 117 | tasks.append(self.send_custom_message(message, peer)) 118 | result = await asyncio.gather(*tasks, return_exceptions=ignore_errors) 119 | failed = 0 120 | for r in result: 121 | if isinstance(r, Exception): 122 | failed += 1 123 | self.logger.debug(f'Spread broadcast: {failed} failed out of {len(result)}') 124 | 125 | def get_signed_myself(self): 126 | ts = int(time.time()) 127 | 128 | overlay_node_data = {'id': {'@type': 'pub.ed25519', 'key': self.client.ed25519_public.encode().hex()}, 129 | 'overlay': self.overlay_id, 'version': ts, 'signature': b''} 130 | 131 | overlay_node_to_sign = self.schemas.serialize(self.schemas.get_by_name('overlay.node.toSign'), 132 | {'id': {'id': self.client.get_key_id().hex()}, 133 | 'overlay': self.overlay_id, 134 | 'version': overlay_node_data['version']}) 135 | signature = self.client.sign(overlay_node_to_sign) 136 | 137 | overlay_node = overlay_node_data | {'signature': signature} 138 | return overlay_node 139 | 140 | async def send_query_message(self, tl_schema_name: str, data: dict, peer: Node) -> typing.List[typing.Union[dict, bytes]]: 141 | """ 142 | :param tl_schema_name: 143 | :param data: 144 | :param peer: 145 | :return: dict if response was known TL schema, bytes otherwise 146 | """ 147 | 148 | message = { 149 | '@type': 'adnl.message.query', 150 | 'query_id': get_random(32), 151 | 'query': self.schemas.serialize(self.schemas.get_by_name('overlay.query'), data={'overlay': self.overlay_id}) 152 | + self.schemas.serialize(self.schemas.get_by_name(tl_schema_name), data) 153 | } 154 | data = { 155 | 'message': message, 156 | } 157 | 158 | result = await self.send_message_in_channel(data, None, peer) 159 | return result 160 | 161 | async def send_custom_message(self, message: typing.Union[dict, bytes], peer: Node) -> list: 162 | 163 | custom_message = { 164 | '@type': 'adnl.message.custom', 165 | 'data': (self.schemas.serialize(self.schemas.get_by_name('overlay.message'), data={'overlay': self.overlay_id}) + 166 | self.schemas.serialize(self.schemas.get_by_name(message['@type']), message)) 167 | } 168 | 169 | data = { 170 | 'message': custom_message, 171 | } 172 | 173 | result = await self.send_message_in_channel(data, None, peer) 174 | return result 175 | 176 | def get_message_with_overlay_prefix(self, schema_name: str, data: dict) -> bytes: 177 | return (self.schemas.serialize( 178 | schema=self.schemas.get_by_name('overlay.query'), 179 | data={'overlay': self.overlay_id}) 180 | + self.schemas.serialize( 181 | schema=self.schemas.get_by_name(schema_name), 182 | data=data) 183 | ) 184 | 185 | def _get_default_message(self): 186 | peers = [ 187 | self.get_signed_myself() 188 | ] 189 | return { 190 | '@type': 'adnl.message.query', 191 | 'query_id': get_random(32), 192 | 'query': self.get_message_with_overlay_prefix('overlay.getRandomPeers', {'peers': {'nodes': peers}}) 193 | } 194 | 195 | async def get_random_peers(self, peer: OverlayNode): 196 | overlay_node = self.get_signed_myself() 197 | 198 | peers = [ 199 | overlay_node 200 | ] 201 | return await self.send_query_message(tl_schema_name='overlay.getRandomPeers', data={'peers': {'nodes': peers}}, 202 | peer=peer) 203 | 204 | async def get_capabilities(self, peer: OverlayNode): 205 | return await self.send_query_message(tl_schema_name='tonNode.getCapabilities', data={}, peer=peer) 206 | 207 | async def raw_download_block(self, block: BlockIdExt, peer: OverlayNode) -> bytes: 208 | """ 209 | :param block: 210 | :param peer: 211 | :return: block boc 212 | """ 213 | return (await self.send_query_message(tl_schema_name='tonNode.downloadBlock', 214 | data={'block': block.to_dict()}, peer=peer))[0] 215 | 216 | async def download_block(self, block: BlockIdExt, peer: OverlayNode) -> Block: 217 | """ 218 | :param block: 219 | :param peer: 220 | :return: deserialized block 221 | """ 222 | blk_boc = await self.raw_download_block(block, peer) 223 | return Block.deserialize(Slice.one_from_boc(blk_boc)) 224 | 225 | async def prepare_block(self, block: BlockIdExt, peer: OverlayNode) -> dict: 226 | return (await self.send_query_message(tl_schema_name='tonNode.prepareBlock', 227 | data={'block': block.to_dict()}, peer=peer))[0] 228 | -------------------------------------------------------------------------------- /pytoniq/contract/__init__.py: -------------------------------------------------------------------------------- 1 | from .contract import Contract, ContractError 2 | from .wallets import * 3 | from .nft import * 4 | -------------------------------------------------------------------------------- /pytoniq/contract/contract.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from ..liteclient import LiteClientLike 4 | from pytoniq_core.boc.cell import Cell 5 | from pytoniq_core.boc.address import Address 6 | from pytoniq_core.tlb.account import StateInit, Account, SimpleAccount, ShardAccount 7 | from pytoniq_core.tlb.block import CurrencyCollection 8 | from pytoniq_core.tlb.transaction import ExternalMsgInfo, MessageAny, InternalMsgInfo 9 | 10 | 11 | class ContractError(BaseException): 12 | pass 13 | 14 | 15 | class Contract: 16 | 17 | def __init__(self, provider: LiteClientLike, address: Address, account: typing.Optional[Account] = None, 18 | shard_account: typing.Optional[ShardAccount] = None, state_init: typing.Optional[StateInit] = None, 19 | **kwargs): 20 | """ 21 | :param provider: LiteClient 22 | :param address: contract address 23 | :param account: full account state 24 | :param shard_account: shard account 25 | :param state_init: account state init. usually used for contract deploying 26 | :param kwargs: some additional account attributes. for e.g. private key for wallet contracts 27 | """ 28 | self.provider = provider 29 | self.address = address 30 | self.raw_account: typing.Optional[Account] = None 31 | self.account: SimpleAccount = None 32 | self.shard_account: typing.Optional[ShardAccount] = None 33 | self.is_active: bool = None 34 | self.is_uninitialized: bool = None 35 | self.is_frozen: bool = None 36 | self.state: typing.Optional[StateInit] = None 37 | self.state_init: typing.Optional[StateInit] = state_init 38 | self.set_account_attributes(account, shard_account) 39 | 40 | for k, v in kwargs.items(): 41 | setattr(self, k, v) 42 | 43 | def set_account_attributes(self, account: typing.Optional[Account], shard_account: typing.Optional[ShardAccount]): 44 | self.raw_account = account 45 | self.account = SimpleAccount.from_raw(account, self.address) 46 | self.shard_account = shard_account 47 | self.is_active = self.account.is_active() 48 | self.is_uninitialized = self.account.is_uninitialized() 49 | self.is_frozen = self.account.is_frozen() 50 | 51 | self.state = None 52 | if self.is_active: 53 | self.state = self.account.state.state_init 54 | else: 55 | self.state = self.state_init 56 | 57 | @property 58 | def data(self) -> Cell: 59 | return self.state.data 60 | 61 | @property 62 | def code(self) -> Cell: 63 | return self.state.code 64 | 65 | @property 66 | def balance(self) -> int: 67 | """ 68 | :return: balance from saved account state 69 | """ 70 | return self.account.balance 71 | 72 | @classmethod 73 | async def from_address(cls, provider: LiteClientLike, address: typing.Union[str, Address], **kwargs): 74 | if isinstance(address, str): 75 | address = Address(address) 76 | account, shard_account = await provider.raw_get_account_state(address) 77 | return cls(provider=provider, address=address, account=account, shard_account=shard_account, **kwargs) 78 | 79 | @classmethod 80 | async def from_state_init(cls, provider: LiteClientLike, workchain: int, state_init: StateInit, **kwargs): 81 | address = Address((workchain, state_init.serialize().hash)) 82 | return await cls.from_address(provider=provider, address=address, state_init=state_init, **kwargs) 83 | 84 | @classmethod 85 | async def from_code_and_data(cls, provider: LiteClientLike, workchain: int, code: Cell, data: Cell, **kwargs): 86 | state_init = StateInit(code=code, data=data) 87 | return await cls.from_state_init(provider=provider, workchain=workchain, state_init=state_init, **kwargs) 88 | 89 | async def update(self): 90 | account, shard_account = await self.provider.raw_get_account_state(self.address) 91 | self.set_account_attributes(account, shard_account) 92 | 93 | async def raw_get_account_state(self): 94 | return (await self.provider.raw_get_account_state(address=self.address))[0] 95 | 96 | async def get_account_state(self): 97 | return await self.provider.get_account_state(self.address) 98 | 99 | async def get_balance(self) -> int: 100 | """ 101 | :return: balance from current account state. better to 102 | await contract.update() 103 | contract.balance 104 | """ 105 | account_state = await self.get_account_state() 106 | return account_state.balance 107 | 108 | async def run_get_method(self, method: typing.Union[str, int], stack: typing.Optional[list] = None): 109 | if stack is None: 110 | stack = [] 111 | return await self.provider.run_get_method(self.address, method, stack) 112 | 113 | @staticmethod 114 | def create_external_msg(src: typing.Optional[Address] = None, dest: typing.Optional[Address] = None, 115 | import_fee: int = 0, state_init: typing.Optional[StateInit] = None, 116 | body: Cell = None) -> MessageAny: 117 | info = ExternalMsgInfo(src, dest, import_fee) 118 | if body is None: 119 | body = Cell.empty() 120 | message = MessageAny(info=info, init=state_init, body=body) 121 | return message 122 | 123 | @staticmethod 124 | def create_internal_msg(ihr_disabled: bool = True, bounce: bool = None, bounced: bool = False, src: Address = None, 125 | dest: Address = None, 126 | value: typing.Union[CurrencyCollection, int] = 0, ihr_fee: int = 0, fwd_fee: int = 0, 127 | created_lt: int = 0, 128 | created_at: int = 0, state_init: typing.Optional[StateInit] = None, 129 | body: Cell = None) -> MessageAny: 130 | if isinstance(value, int): 131 | value = CurrencyCollection(grams=value, other=None) 132 | if bounce is None: 133 | bounce = dest.is_bounceable 134 | info = InternalMsgInfo(ihr_disabled, bounce, bounced, src, dest, value, ihr_fee, fwd_fee, created_lt, created_at) 135 | if body is None: 136 | body = Cell.empty() 137 | message = MessageAny(info=info, init=state_init, body=body) 138 | return message 139 | 140 | async def send_external(self, src: typing.Optional[Address] = None, import_fee: int = 0, 141 | state_init: typing.Optional[StateInit] = None, body: Cell = None): 142 | message = self.create_external_msg(src=src, dest=self.address, import_fee=import_fee, state_init=state_init, 143 | body=body) 144 | return await self.provider.raw_send_message(message.serialize().to_boc()) 145 | 146 | async def send_init_external(self): 147 | if not self.state_init: 148 | raise ContractError('contract does not have state_init attribute') 149 | return await self.send_external(state_init=self.state_init) 150 | 151 | async def deploy_via_external(self): 152 | return await self.send_init_external() 153 | 154 | def __repr__(self): 155 | return f'<{self.account.state.type_} {self.__class__.__name__} {self.address}>' # 156 | -------------------------------------------------------------------------------- /pytoniq/contract/nft/__init__.py: -------------------------------------------------------------------------------- 1 | from .nft import NftItem 2 | -------------------------------------------------------------------------------- /pytoniq/contract/nft/nft.py: -------------------------------------------------------------------------------- 1 | from ..contract import Contract, ContractError 2 | from ...liteclient import LiteClientLike 3 | from pytoniq_core.boc import Cell, Builder, HashMap 4 | from pytoniq_core.boc.address import Address 5 | from pytoniq_core.tlb.custom.nft import NftItemData 6 | 7 | NFT_CODE = Cell.one_from_boc(b'\xb5\xee\x9crA\x02\r\x01\x00\x01\xd0\x00\x01\x14\xff\x00\xf4\xa4\x13\xf4\xbc\xf2\xc8\x0b\x01\x02\x01b\x02\x03\x02\x02\xce\x04\x05\x00\t\xa1\x1f\x9f\xe0\x05\x02\x01 \x06\x07\x02\x01 \x0b\x0c\x02\xd7\x0c\x88q\xc0$\x97\xc0\xf844\xc0\xc0\\l$\x97\xc0\xf8>\x90>\x90\x0c~\x80\x0c\\u\xc8~\x80\x0c~\x80\x0c<\x00\x81,\xe3\x85\x0c\x1b\x08\x8d\x14\x8c\xb1\xc1|\xb8e@~\x905\x0c\x04\x08\xfc\x00\xf8\x01\xb4\xc7\xf4\xcf\xe0\x84\x17\xf3\x0fE\x14\x8c.\xa3\xa1\xcc\x84\r\xd7\x8c\x90\x04\xf8\x0c\r\r\rM`\x84\x0b\xf2\xc9\xa8\x84\xae\xb8\xc0\x97\xc1!\x03\xfc\xbc \x08\t\x00\x11>\x91\x0c\x1c.\xbc\xb8S`\x01\xf6Q5\xc7\x05\xf2\xe1\x91\xfa@!\xf0\x01\xfa@\xd2\x001\xfa\x00\x82\n\xfa\xf0\x80\x1b\xa1!\x94S\x15\xa0\xa1\xde"\xd7\x0b\x01\xc3\x00 \x92\x06\xa1\x916\xe2 \xc2\xff\xf2\xe1\x92!\x8e>\x82\x10\x05\x13\x8d\x91\xc8P\t\xcf\x16P\x0b\xcf\x16q$I\x14TF\xa0p\x80\x10\xc8\xcb\x05P\x07\xcf\x16P\x05\xfa\x02\x15\xcbj\x12\xcb\x1f\xcb?"n\xb3\x94X\xcf\x17\x01\x912\xe2\x01\xc9\x01\xfb\x00\x10G\x94\x10*7[\xe2\n\x00rp\x82\x10\x8bw\x175\x05\xc8\xcb\xffP\x04\xcf\x16\x10$\x80@p\x80\x10\xc8\xcb\x05P\x07\xcf\x16P\x05\xfa\x02\x15\xcbj\x12\xcb\x1f\xcb?"n\xb3\x94X\xcf\x17\x01\x912\xe2\x01\xc9\x01\xfb\x00\x00\x82\x02\x8e5&\xf0\x01\x82\x10\xd52v\xdb\x107D\x00mqp\x80\x10\xc8\xcb\x05P\x07\xcf\x16P\x05\xfa\x02\x15\xcbj\x12\xcb\x1f\xcb?"n\xb3\x94X\xcf\x17\x01\x912\xe2\x01\xc9\x01\xfb\x00\x93024\xe2U\x02\xf0\x03\x00;;Q44\xcf\xfe\x90\x085\xd2p\x80&\x9f\xc0~\x905\x0c\x04\t\x04\x08\xf8\x0c\x1c\x16[[`\x00\x1d\x00\xf22\xcf\xd63\xc5\x80s\xc5\xb32{U \xbfu\x04\x1b') 8 | 9 | 10 | class NftItem(Contract): 11 | 12 | @classmethod 13 | async def from_data(cls, provider: LiteClientLike, index: int, collection_address: Address, owner_address: Address, content: Cell, wc: int = 0, **kwargs) -> "NftItem": 14 | data = cls.create_data_cell(index, collection_address, owner_address, content) 15 | return await super().from_code_and_data(provider, wc, NFT_CODE, data, **kwargs) 16 | 17 | @staticmethod 18 | def create_data_cell(index: int, collection_address: Address, owner_address: Address, content: Cell) -> Cell: 19 | return NftItemData(index=index, collection_address=collection_address, owner_address=owner_address, content=content).serialize() 20 | 21 | @property 22 | def index(self) -> int: 23 | """ 24 | :return: index taken from contract data 25 | """ 26 | return NftItemData.deserialize(self.state.data.begin_parse()).index 27 | 28 | @property 29 | def collection_address(self) -> Address: 30 | """ 31 | :return: collection_address taken from contract data 32 | """ 33 | return NftItemData.deserialize(self.state.data.begin_parse()).collection_address 34 | 35 | @property 36 | def owner_address(self) -> Address: 37 | """ 38 | :return: owner_address taken from contract data 39 | """ 40 | return NftItemData.deserialize(self.state.data.begin_parse()).owner_address 41 | 42 | @property 43 | def content(self) -> Cell: 44 | """ 45 | :return: old_queries taken from contract data 46 | """ 47 | return NftItemData.deserialize(self.state.data.begin_parse()).content 48 | 49 | async def get_nft_data(self) -> bool: 50 | return await super().run_get_method(method='get_nft_data', stack=[]) 51 | -------------------------------------------------------------------------------- /pytoniq/contract/nft/nft_sale.py: -------------------------------------------------------------------------------- 1 | import time 2 | import typing 3 | 4 | from ..contract import Contract, ContractError 5 | from ...liteclient import LiteClientLike 6 | from pytoniq_core.boc import Cell, Builder, HashMap 7 | from pytoniq_core.boc.address import Address 8 | from pytoniq_core.tlb.custom.nft import NftItemSaleData, NftItemSaleFees 9 | 10 | 11 | # https://github.com/getgems-io/nft-contracts/blob/main/packages/contracts/sources/nft-fixprice-sale-v3r2.fc 12 | NFT_SALE_CODE = Cell.one_from_boc(b"\xb5\xee\x9crA\x02\x0b\x01\x00\x02\xb9\x00\x01\x14\xff\x00\xf4\xa4\x13\xf4\xbc\xf2\xc8\x0b\x01\x02\x01 \x02\x03\x02\x01H\x04\x05\x00~\xf20\xedD\xd0\xd3\x00\xd3\x1f\xfa@\xfa@\xfa@\xfa\x00\xd4\xd3\x000\xc0\x01\x8e\x1d\xf8\x00p\x07\xc8\xcb\x00\x16\xcb\x1fP\x04\xcf\x16X\xcf\x16\x01\xcf\x16\x01\xfa\x02\xcc\xcb\x00\xc9\xedT\xe0_\x07\x82\x00\xff\xfe\xf2\xf0\x02\x02\xcd\x06\x07\x00W\xa08Y\xda\x89\xa1\xa6\x01\xa6?\xf4\x81\xf4\x81\xf4\x81\xf4\x01\xa9\xa6\x00`a\xa1\xf4\x81\xf4\x01\xf4\x81\xf4\x00a\x04 \x8c\x92\xb0\xa0\x15\x80\x02\xab\x01\x01\xf7\xd0\x0e\x86\x98\x18\x0b\x8d\x84\x92\xf8'\x07\xd2\x01\x87j&\x86\x98\x06\x98\xff\xd2\x07\xd2\x07\xd2\x07\xd0\x06\xa6\x98\x01\x81\x83\x82\x98N8\x06\x00\x04\xa9\x88N\x98\xf8V\xf1\x0e\x18\x04\xa1\x80N\x99\xfcp\x8c[1\xb0\xb71\xb2\xb6A^8,\x93\x99\x96\xf2\x80W\x11V\x00\x0c\x92\xf8o\x01&\xbaN\x10\x11\\\x08\x11]\xd1V\x00\t\x15\x9d\x8d\x82\x9d\xc68-\x84\xe8\xea\xf8n\xa1\x86\x86\x98>\xa1\x80\x0f\xd8\x07\x01N\x00\x0c\x08\x01\xf7f\x08@\xeek(\x01I\x82\x81H\xc2\xfb\xcb\x87\x08\x93C\xe9\x03\xe8\x03\xe9\x03\xe8\x00\xc1NJ\x84\x86\x85B\x1e\x84Z\x81JA\xc2\x00C#,\x15@\x0f "NftItemSale": 19 | data = cls.create_data_cell(is_complete=False, created_at=int(time.time()), marketplace_address=marketplace_address, nft_address=nft_address, nft_owner_address=nft_owner_address, full_price=full_price, fees=fees, can_deploy_by_external=True) 20 | return await super().from_code_and_data(provider, wc, NFT_SALE_CODE, data, **kwargs) 21 | 22 | @staticmethod 23 | def create_data_cell(is_complete: bool, created_at: int, marketplace_address: Address, nft_address: Address, nft_owner_address: Address, full_price: int, fees: NftItemSaleFees, can_deploy_by_external: bool) -> Cell: 24 | return NftItemSaleData(is_complete, created_at, marketplace_address, nft_address, nft_owner_address, full_price, fees, can_deploy_by_external).serialize() 25 | 26 | @staticmethod 27 | def create_fees_cell(marketplace_fee_address: Address, marketplace_fee: int, royalty_address: Address, royalty_amount: int) -> Cell: 28 | return NftItemSaleFees(marketplace_fee_address, marketplace_fee, royalty_address, royalty_amount).serialize() 29 | 30 | @property 31 | def is_complete(self) -> bool: 32 | """ 33 | :return: is_complete taken from contract data 34 | """ 35 | return NftItemSaleData.deserialize(self.state.data.begin_parse()).is_complete 36 | 37 | @property 38 | def created_at(self) -> int: 39 | """ 40 | :return: created_at taken from contract data 41 | """ 42 | return NftItemSaleData.deserialize(self.state.data.begin_parse()).created_at 43 | 44 | @property 45 | def marketplace_address(self) -> Address: 46 | """ 47 | :return: marketplace_address taken from contract data 48 | """ 49 | return NftItemSaleData.deserialize(self.state.data.begin_parse()).marketplace_address 50 | 51 | @property 52 | def nft_address(self) -> Address: 53 | """ 54 | :return: nft_address taken from contract data 55 | """ 56 | return NftItemSaleData.deserialize(self.state.data.begin_parse()).nft_address 57 | 58 | @property 59 | def nft_owner_address(self) -> Address: 60 | """ 61 | :return: nft_owner_address taken from contract data 62 | """ 63 | return NftItemSaleData.deserialize(self.state.data.begin_parse()).nft_owner_address 64 | 65 | @property 66 | def full_price(self) -> int: 67 | """ 68 | :return: full_price taken from contract data 69 | """ 70 | return NftItemSaleData.deserialize(self.state.data.begin_parse()).full_price 71 | 72 | @property 73 | def fees_cell(self) -> NftItemSaleFees: 74 | """ 75 | :return: fees_cell taken from contract data 76 | """ 77 | return NftItemSaleData.deserialize(self.state.data.begin_parse()).fees_cell 78 | 79 | @property 80 | def can_deploy_by_external(self) -> bool: 81 | """ 82 | :return: can_deploy_by_external taken from contract data 83 | """ 84 | return NftItemSaleData.deserialize(self.state.data.begin_parse()).can_deploy_by_external 85 | 86 | async def get_sale_data(self) -> bool: 87 | return await super().run_get_method(method='get_sale_data', stack=[]) 88 | -------------------------------------------------------------------------------- /pytoniq/contract/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def generate_query_id(offset: int = 7200): 5 | return int(time.time() + offset) << 32 6 | 7 | 8 | -------------------------------------------------------------------------------- /pytoniq/contract/wallets/__init__.py: -------------------------------------------------------------------------------- 1 | from .wallet import WalletError, Wallet, BaseWallet, WalletV3, WalletV4, WalletV3R1, WalletV3R2, WalletV4R2, WalletV3Data, WalletV4Data, WalletMessage 2 | from .highload import HighloadWallet 3 | from .highload_v3 import HighloadWalletV3 4 | -------------------------------------------------------------------------------- /pytoniq/contract/wallets/highload.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import typing 3 | 4 | from .wallet import Wallet, WalletError 5 | from ..utils import generate_query_id 6 | from ..contract import Contract, ContractError 7 | from ...liteclient import LiteClientLike 8 | from pytoniq_core.crypto.keys import private_key_to_public_key, mnemonic_to_private_key, mnemonic_is_valid, mnemonic_new 9 | from pytoniq_core.crypto.signature import sign_message 10 | from pytoniq_core.boc import Cell, Builder, HashMap 11 | from pytoniq_core.boc.address import Address 12 | from pytoniq_core.tlb.account import StateInit 13 | from pytoniq_core.tlb.custom.wallet import HighloadWalletData, WalletMessage 14 | 15 | HIGHLOAD_WALLET_CODE = Cell.one_from_boc( 16 | b'\xb5\xee\x9cr\x01\x01\t\x01\x00\xe5\x00\x01\x14\xff\x00\xf4\xa4\x13\xf4\xbc\xf2\xc8\x0b\x01\x02\x01 \x02\x03\x02\x01H\x04\x05\x01\xea\xf2\x83\x08\xd7\x18 \xd3\x1f\xd3?\xf8#\xaa\x1fS \xb9\xf2c\xedD\xd0\xd3\x1f\xd3?\xd3\xff\xf4\x04\xd1S`\x80@\xf4\x0eo\xa11\xf2`Qs\xba\xf2\xa2\x07\xf9\x01T\x10\x87\xf9\x10\xf2\xa3\x02\xf4\x04\xd1\xf8\x00\x7f\x8e\x16!\x80\x10\xf4xo\xa5 \x98\x02\xd3\x07\xd40\x01\xfb\x00\x912\xe2\x01\xb3\xe6[\x83%\xa1\xc8@4\x80@\xf4C\x8a\xe61\x01\xc8\xcb\x1f\x13\xcb?\xcb\xff\xf4\x00\xc9\xedT\x08\x00\x04\xd00\x02\x01 \x06\x07\x00\x17\xbd\x9c\xe7j&\x86\x9a\xf9\x8e\xb8_\xfc\x00A\xbe_\x97j&\x86\x98\xf9\x8e\x99\xfe\x9f\xf9\x8f\xa0&\x8a\x91\x04\x02\x07\xa0s}\t\x8c\x92\xdb\xfc\x95\xdd\x1f\x14\x004 \x80@\xf4\x96o\xa5l\x12 \x940S\x03\xb9\xde \x9336\x01\x92l!\xe2\xb3') 17 | 18 | 19 | class HighloadWallet(Wallet): 20 | 21 | @classmethod 22 | async def from_data(cls, provider: LiteClientLike, public_key: bytes, wc: int = 0, 23 | wallet_id: typing.Optional[int] = None, **kwargs) -> "HighloadWallet": 24 | data = cls.create_data_cell(public_key, wallet_id, wc) 25 | return await super().from_code_and_data(provider, wc, HIGHLOAD_WALLET_CODE, data, **kwargs) 26 | 27 | @staticmethod 28 | def create_data_cell(public_key: bytes, wallet_id: typing.Optional[int] = None, wc: typing.Optional[int] = 0, 29 | old_queries: typing.Optional[dict] = None) -> Cell: 30 | if wallet_id is None: 31 | wallet_id = 698983191 + wc 32 | return HighloadWalletData(wallet_id=wallet_id, public_key=public_key, last_cleaned=0, old_queries=old_queries).serialize() 33 | 34 | @classmethod 35 | async def from_private_key(cls, provider: LiteClientLike, private_key: bytes, wc: int = 0, 36 | wallet_id: typing.Optional[int] = None): 37 | logging.getLogger('pytoniq').warning('HighloadWallet (v2) is deprecated, use HighloadWalletV3 instead') 38 | public_key = private_key_to_public_key(private_key) 39 | return await cls.from_data(provider=provider, wc=wc, public_key=public_key, wallet_id=wallet_id, 40 | private_key=private_key) 41 | 42 | @classmethod 43 | async def from_mnemonic(cls, provider: LiteClientLike, mnemonics: typing.Union[list, str], wc: int = 0, 44 | wallet_id: typing.Optional[int] = None): 45 | if isinstance(mnemonics, str): 46 | mnemonics = mnemonics.split() 47 | assert mnemonic_is_valid(mnemonics), 'mnemonics are invalid!' 48 | _, private_key = mnemonic_to_private_key(mnemonics) 49 | return await cls.from_private_key(provider, private_key, wc, wallet_id) 50 | 51 | @classmethod 52 | async def create(cls, provider: LiteClientLike, wc: int = 0, wallet_id: typing.Optional[int] = None): 53 | """ 54 | :param provider: provider 55 | :param wc: wallet workchain 56 | :param wallet_id: subwallet_id 57 | :return: mnemonics and Wallet instance of provided version 58 | """ 59 | mnemo = mnemonic_new(24) 60 | return mnemo, await cls.from_mnemonic(provider, mnemo, wc, wallet_id) 61 | 62 | @staticmethod 63 | def raw_create_transfer_msg(private_key: bytes, wallet_id: int, messages: typing.List[WalletMessage], 64 | query_id: int = 0, offset: int = 7200) -> Cell: 65 | 66 | signing_message = Builder().store_uint(wallet_id, 32) 67 | if not query_id: 68 | signing_message.store_uint(generate_query_id(offset), 64) 69 | else: 70 | signing_message.store_uint(query_id, 64) 71 | 72 | def value_serializer(src, dest): 73 | dest.store_cell(src.serialize()) 74 | 75 | messages_dict = HashMap(key_size=16, value_serializer=value_serializer) 76 | 77 | for i in range(len(messages)): 78 | messages_dict.set_int_key(i, messages[i]) 79 | 80 | signing_message.store_dict(messages_dict.serialize()) 81 | 82 | signing_message = signing_message.end_cell() 83 | signature = sign_message(signing_message.hash, private_key) 84 | return Builder() \ 85 | .store_bytes(signature) \ 86 | .store_cell(signing_message) \ 87 | .end_cell() 88 | 89 | async def raw_transfer(self, msgs: typing.List[WalletMessage], query_id: int = 0, offset: int = 7200): 90 | """ 91 | :param query_id: query id 92 | :param offset: if query id is 0 it will be generated as current_time + offset 93 | :param msgs: list of WalletMessages. to create one call create_wallet_internal_message meth 94 | """ 95 | assert len(msgs) <= 254, 'for highload wallet maximum messages amount is 254' 96 | if 'private_key' not in self.__dict__: 97 | raise WalletError('must specify wallet private key!') 98 | 99 | transfer_msg = self.raw_create_transfer_msg(private_key=self.private_key, wallet_id=self.wallet_id, 100 | query_id=query_id, offset=offset, messages=msgs) 101 | 102 | return await self.send_external(body=transfer_msg) 103 | 104 | async def transfer(self, destinations: typing.Union[typing.List[Address], typing.List[str]], 105 | amounts: typing.List[int], 106 | bodies: typing.List[Cell], 107 | state_inits: typing.List[StateInit] = None): 108 | # Check if all lists are of the same length 109 | if not (len(destinations) == len(amounts) == len(bodies)): 110 | raise ValueError("All lists (destinations, amounts, bodies) must be of the same length.") 111 | 112 | # Initialize state_inits if None 113 | if state_inits is None: 114 | state_inits = [None] * len(destinations) 115 | elif len(state_inits) != len(destinations): 116 | raise ValueError("Length of state_inits must match the length of destinations.") 117 | 118 | result_msgs = [] 119 | for i in range(len(destinations)): 120 | destination = destinations[i] 121 | body = bodies[i] if bodies[i] is not None else Cell.empty() 122 | 123 | if isinstance(destination, str): 124 | destination = Address(destination) 125 | 126 | result_msgs.append( 127 | self.create_wallet_internal_message(destination=destination, value=amounts[i], 128 | body=body, state_init=state_inits[i])) 129 | return await self.raw_transfer(msgs=result_msgs) 130 | 131 | async def send_init_external(self): 132 | if not self.state_init: 133 | raise ContractError('contract does not have state_init attribute') 134 | if 'private_key' not in self.__dict__: 135 | raise WalletError('must specify wallet private key!') 136 | body = self.raw_create_transfer_msg(private_key=self.private_key, wallet_id=self.wallet_id, messages=[]) 137 | return await self.send_external(state_init=self.state_init, body=body) 138 | 139 | @property 140 | def wallet_id(self) -> int: 141 | """ 142 | :return: wallet_id taken from contract data 143 | """ 144 | return HighloadWalletData.deserialize(self.state.data.begin_parse()).wallet_id 145 | 146 | @property 147 | def last_cleaned(self) -> int: 148 | """ 149 | :return: last_cleaned taken from contract data 150 | """ 151 | return HighloadWalletData.deserialize(self.state.data.begin_parse()).last_cleaned 152 | 153 | @property 154 | def public_key(self) -> bytes: 155 | """ 156 | :return: public_key taken from contract data 157 | """ 158 | return HighloadWalletData.deserialize(self.state.data.begin_parse()).public_key 159 | 160 | @property 161 | def old_queries(self) -> dict: 162 | """ 163 | :return: old_queries taken from contract data 164 | """ 165 | return HighloadWalletData.deserialize(self.state.data.begin_parse()).old_queries 166 | 167 | async def processed(self, query_id: int) -> bool: 168 | """ 169 | :return: is query processed from wallet's get method 170 | """ 171 | return (await super().run_get_method(method='processed?', stack=[query_id]))[0] 172 | -------------------------------------------------------------------------------- /pytoniq/contract/wallets/highload_v3.py: -------------------------------------------------------------------------------- 1 | import time 2 | import typing 3 | 4 | from .wallet import Wallet, WalletError 5 | from ..contract import ContractError 6 | from ...liteclient import LiteClientLike 7 | from pytoniq_core.crypto.keys import private_key_to_public_key, mnemonic_to_private_key, mnemonic_is_valid, mnemonic_new 8 | from pytoniq_core.crypto.signature import sign_message 9 | from pytoniq_core.boc import Cell, Builder, begin_cell 10 | from pytoniq_core.boc.address import Address 11 | from pytoniq_core.tlb.account import StateInit 12 | from pytoniq_core.tlb.custom.wallet import HighloadWalletV3Data, WalletMessage 13 | 14 | HIGHLOAD_V3_WALLET_CODE = Cell.one_from_boc( 15 | b'\xb5\xee\x9crA\x02\x10\x01\x00\x02(\x00\x01\x14\xff\x00\xf4\xa4\x13\xf4\xbc\xf2\xc8\x0b\x01\x02\x01 \x02\r\x02\x01H\x03\x04\x00x\xd0 \xd7K\xc0\x01\x01\xc0`\xb0\x91[\xe1\x01\xd0\xd3\x03\x01q\xb0\x91[\xe0\xfa@0\xf8(\xc7\x05\xb3\x910\xe0\xd3\x1f\x01\x82\x10\xaeB\xe5\xa4\xba\x9d\x80@\xd7!\xd7L\xf8*\x01\xedU\xfb\x04\xe00\x02\x01 \x05\n\x02\x02s\x06\x07\x00\x11\xad\xcev\xa2hk\x85\xff\xc0\x02\x01 \x08\t\x00\x1a\xab\xb6\xedD\xd0\x81\x01"\xd7!\xd7\x0b?\x00\x18\xaa;\xedD\xd0\x83\x07\xd7!\xd7\x0b\x1f\x02\x01 \x0b\x0c\x00\x1b\xb9\xa6\xee\xd4M\x08\x10\x16-r\x1dp\xb1X\x00\xe5\xb8\xbf.\xda.\xdf\xb2\x1a\xb0\x90(@\x9b\x0e\xd4M\x08\x10\x12\rr\x1f@O@M3\xfd1]\x10X\xe1\xbf\x822Z\x15!\x0b\x99\xf3&\xdf\x820Z\xa0\x01Z\x11+\x99#\x06\xdd\xe9#\x03>)#\x03>%#\x08\x00\xdf@\xf6\xfa\x19\xed\x02\x1dr\x1dp\xa0\tU\xf07\xfd\xb3\x1e\t\x13\x0e%\x98\x00\xdf@\xf6\xfa\x19\xcd\x00\x1dr\x1dp\xa0\t7\xfd\xb3\x1e\t\x15\xbe\'\x08\x01\xf6\xf2\xd4\x83\x08\xd7\x18\xd1!\xf9\x00\xedD\xd0\xd3\xff\xd3\x1f\xf4\x04\xf4\x04\xd3?\xd3\x15\xd1\xf8#!\xa1R \xb9\x8e\x123m\xf8#$\xaa\x00\xa1\x12\xb9\x92m2\xdeX\xf8#\x01\xdeT\x16u\xf9\x10\xf2\xa1\x06\xd0\xd3\x1f\xd4\xd3\x07\xd3\x0c\xd3\t\xd3?\xd3\x15\xd1Qh\xba\xf2\xa2QZ\xba\xf2\xa6\xf8#*\xa1RP\xbc\xf2\xa3\x04\xf8#\xbb\xf2\xa3S\x04\x80\r\xf4\x0fo\xa1\x99\xd0$\xd7!\xd7\n\x00\xf2d\x910\xe2\x0e\x01\xfeS\t\x80\r\xf4\x0fo\xa1\x8e\x13\xd0P\x04\xd7\x18\xd2\x00\x01\xf2d\xc8X\xcf\x16\xcf\x83\x01\xcf\x16\x8e\x100\xc8$\xcf@\xcf\x83\x84\tP\x05\xa1\xa5\x14\xcf@\xe2\xf8\x00\xc9@9\x80\r\xf4\x17\x04\xc8\xcb\xff\x13\xcb\x1f\xf4\x00\x12\xf4\x00\x12\xcb?\x12\xcb\x15\xc9\xedT\xf8\x0f!\xd0\xd3\x00\x01\xf2e\xd3\x02\x01q\xb0\x92_\x03\xe0\xfa@\x01\xd7\x0b\x01\xc0\x00\xf2\xa5\xfa@1\xfa\x001\xf4\x01\xfa\x001\xfa\x001\x80`\xd7!\xd3\x00\x01\x0f\x00 \xf2e\xd2\x00\x01\x93\xd41\xd1\x910\xe2r\xb1\xfb\x00\xb5\x85\xbf\x03') 16 | 17 | 18 | class HighloadWalletV3(Wallet): 19 | 20 | @classmethod 21 | async def from_data(cls, provider: LiteClientLike, public_key: bytes, wc: int = 0, 22 | wallet_id: typing.Optional[int] = None, **kwargs) -> "HighloadWalletV3": 23 | data = cls.create_data_cell(public_key, wallet_id, wc) 24 | return await super().from_code_and_data(provider, wc, HIGHLOAD_V3_WALLET_CODE, data, **kwargs) 25 | 26 | @staticmethod 27 | def create_data_cell(public_key: bytes, wallet_id: typing.Optional[int] = None, wc: typing.Optional[int] = 0, 28 | old_queries: typing.Optional[dict] = None, queries: typing.Optional[dict] = None, timeout: typing.Optional[int] = 3600) -> Cell: 29 | if wallet_id is None: 30 | wallet_id = 0x10ad + wc 31 | return HighloadWalletV3Data(wallet_id=wallet_id, public_key=public_key, last_clean_time=0, old_queries=old_queries, queries=queries, timeout=timeout).serialize() 32 | 33 | @classmethod 34 | async def from_private_key(cls, provider: LiteClientLike, private_key: bytes, wc: int = 0, 35 | wallet_id: typing.Optional[int] = None, timeout: typing.Optional[int] = 3600): 36 | public_key = private_key_to_public_key(private_key) 37 | return await cls.from_data(provider=provider, wc=wc, public_key=public_key, wallet_id=wallet_id, 38 | private_key=private_key) 39 | 40 | @classmethod 41 | async def from_mnemonic(cls, provider: LiteClientLike, mnemonics: typing.Union[list, str], wc: int = 0, 42 | wallet_id: typing.Optional[int] = None, timeout: typing.Optional[int] = 3600): 43 | if isinstance(mnemonics, str): 44 | mnemonics = mnemonics.split() 45 | assert mnemonic_is_valid(mnemonics), 'mnemonics are invalid!' 46 | _, private_key = mnemonic_to_private_key(mnemonics) 47 | return await cls.from_private_key(provider, private_key, wc, wallet_id) 48 | 49 | @classmethod 50 | async def create(cls, provider: LiteClientLike, wc: int = 0, wallet_id: typing.Optional[int] = None, timeout: typing.Optional[int] = 3600): 51 | """ 52 | :param provider: provider 53 | :param wc: wallet workchain 54 | :param wallet_id: subwallet_id 55 | :return: mnemonics and Wallet instance of provided version 56 | """ 57 | mnemo = mnemonic_new(24) 58 | return mnemo, await cls.from_mnemonic(provider, mnemo, wc, wallet_id) 59 | 60 | def pack_actions( 61 | self, 62 | messages: typing.List[WalletMessage], 63 | query_id: int, 64 | ) -> WalletMessage: 65 | 66 | message_per_pack = 253 67 | 68 | if len(messages) > message_per_pack: 69 | rest = self.pack_actions(messages[message_per_pack:], query_id) 70 | messages = messages[:message_per_pack] + [rest] 71 | 72 | amt = 0 73 | list_cell = Cell.empty() 74 | 75 | for msg in messages: 76 | amt += msg.message.info.value.grams 77 | msg = (begin_cell() 78 | .store_uint(0x0ec3c86d, 32) 79 | .store_uint(msg.send_mode, 8) 80 | .store_ref(msg.message.serialize()) 81 | .end_cell()) 82 | list_cell = ( 83 | begin_cell() 84 | .store_ref(list_cell) 85 | .store_cell(msg) 86 | .end_cell() 87 | ) 88 | 89 | # attach some coins for internal message processing gas fees 90 | fees = 7*10**6 * len(messages) + 10**7 # 0.007 per message + 0.01 for all 91 | amt += fees 92 | 93 | return self.create_wallet_internal_message( 94 | destination=self.address, 95 | send_mode=3, 96 | value=amt, 97 | body=( 98 | begin_cell() 99 | .store_uint(0xae42e5a4, 32) 100 | .store_uint(query_id, 64) 101 | .store_ref(list_cell) 102 | .end_cell() 103 | ) 104 | ) 105 | 106 | def raw_create_transfer_msg(self, private_key: bytes, wallet_id: int, messages: typing.List[WalletMessage], 107 | query_id: int = 0, timeout: int = None, created_at: int = None) -> Cell: 108 | 109 | if created_at is None: 110 | created_at = int(time.time()) - 30 111 | if query_id is None: 112 | query_id = created_at % (1 << 23) 113 | if timeout is None: 114 | timeout = self.timeout 115 | 116 | assert len(messages) > 0, 'messages should not be empty' 117 | assert len(messages) <= 254 * 254, 'for highload v3 wallet maximum messages amount is 254*254' 118 | assert timeout < (1 << 22), 'timeout is too big' 119 | assert timeout > 5, 'timeout is too small' 120 | assert query_id < (1 << 23), 'query id is too big' 121 | assert created_at > 0, 'created_at should be positive' 122 | 123 | if len(messages) == 1 and messages[0].message.init is None: 124 | msg = messages[0] 125 | else: 126 | msg = self.pack_actions(messages, query_id) 127 | 128 | signing_message = ( 129 | begin_cell() 130 | .store_uint(wallet_id, 32) 131 | .store_ref(msg.message.serialize()) 132 | .store_uint(msg.send_mode, 8) 133 | .store_uint(query_id, 23) 134 | .store_uint(created_at, 64) 135 | .store_uint(timeout, 22) 136 | .end_cell() 137 | ) 138 | 139 | signature = sign_message(signing_message.hash, private_key) 140 | return Builder() \ 141 | .store_bytes(signature) \ 142 | .store_ref(signing_message) \ 143 | .end_cell() 144 | 145 | async def raw_transfer(self, msgs: typing.List[WalletMessage], query_id: int = None, timeout: int = None): 146 | if 'private_key' not in self.__dict__: 147 | raise WalletError('must specify wallet private key!') 148 | 149 | transfer_msg = self.raw_create_transfer_msg(private_key=self.private_key, wallet_id=self.wallet_id, 150 | query_id=query_id, timeout=timeout, messages=msgs) 151 | 152 | return await self.send_external(body=transfer_msg) 153 | 154 | async def transfer(self, destinations: typing.Union[typing.List[Address], typing.List[str]], 155 | amounts: typing.List[int], bodies: typing.List[Cell], 156 | state_inits: typing.List[StateInit] = None, query_id: int = None): 157 | # Check if all lists are of the same length 158 | if not (len(destinations) == len(amounts) == len(bodies)): 159 | raise ValueError("All lists (destinations, amounts, bodies) must be of the same length.") 160 | 161 | # Initialize state_inits if None 162 | if state_inits is None: 163 | state_inits = [None] * len(destinations) 164 | elif len(state_inits) != len(destinations): 165 | raise ValueError("Length of state_inits must match the length of destinations.") 166 | 167 | result_msgs = [] 168 | for i in range(len(destinations)): 169 | destination = destinations[i] 170 | body = bodies[i] if bodies[i] is not None else Cell.empty() 171 | 172 | if isinstance(destination, str): 173 | destination = Address(destination) 174 | 175 | result_msgs.append( 176 | self.create_wallet_internal_message(destination=destination, value=amounts[i], 177 | body=body, state_init=state_inits[i])) 178 | return await self.raw_transfer(msgs=result_msgs, query_id=query_id) 179 | 180 | async def send_init_external(self): 181 | if not self.state_init: 182 | raise ContractError('contract does not have state_init attribute') 183 | if 'private_key' not in self.__dict__: 184 | raise WalletError('must specify wallet private key!') 185 | body = self.raw_create_transfer_msg(private_key=self.private_key, wallet_id=self.wallet_id, messages=[self.create_wallet_internal_message(self.address)]) 186 | return await self.send_external(state_init=self.state_init, body=body) 187 | 188 | @property 189 | def wallet_id(self) -> int: 190 | """ 191 | :return: wallet_id taken from contract data 192 | """ 193 | return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).wallet_id 194 | 195 | @property 196 | def last_clean_time(self) -> int: 197 | """ 198 | :return: last_cleaned taken from contract data 199 | """ 200 | return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).last_clean_time 201 | 202 | @property 203 | def timeout(self) -> int: 204 | """ 205 | :return: timeout taken from contract data 206 | """ 207 | return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).timeout 208 | 209 | @property 210 | def public_key(self) -> bytes: 211 | """ 212 | :return: public_key taken from contract data 213 | """ 214 | return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).public_key 215 | 216 | @property 217 | def old_queries(self) -> dict: 218 | """ 219 | :return: old_queries taken from contract data 220 | """ 221 | return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).old_queries 222 | 223 | @property 224 | def queries(self) -> dict: 225 | """ 226 | :return: queries taken from contract data 227 | """ 228 | return HighloadWalletV3Data.deserialize(self.state.data.begin_parse()).queries 229 | 230 | async def get_last_clean_time(self): 231 | """ 232 | :return: wallet's last_clean_time 233 | """ 234 | return (await super().run_get_method(method='get_last_clean_time'))[0] 235 | 236 | async def get_timeout(self): 237 | """ 238 | :return: wallet's timeout 239 | """ 240 | return (await super().run_get_method(method='get_timeout'))[0] 241 | 242 | async def processed(self, query_id: int, need_clean: bool) -> bool: 243 | """ 244 | :return: is query processed from wallet's get method 245 | """ 246 | return (await super().run_get_method(method='processed?', stack=[query_id, -int(need_clean)]))[0] 247 | -------------------------------------------------------------------------------- /pytoniq/contract/wallets/wallet.py: -------------------------------------------------------------------------------- 1 | import time 2 | import typing 3 | 4 | from ...liteclient import LiteClientLike 5 | from ..contract import Contract, ContractError 6 | from pytoniq_core.crypto.keys import private_key_to_public_key, mnemonic_to_private_key, mnemonic_is_valid, mnemonic_new 7 | from pytoniq_core.crypto.signature import sign_message 8 | from pytoniq_core.boc import Cell, Builder 9 | from pytoniq_core.boc.address import Address 10 | from pytoniq_core.tlb.account import StateInit 11 | from pytoniq_core.tlb.custom.wallet import WalletV3Data, WalletV4Data, WalletMessage 12 | 13 | WALLET_V3_R2_CODE = Cell.one_from_boc( 14 | b'\xb5\xee\x9crA\x01\x01\x01\x00q\x00\x00\xde\xff\x00 \xdd \x82\x01L\x97\xba!\x82\x013\x9c\xba\xb1\x9fq\xb0\xedD\xd0\xd3\x1f\xd3\x1f1\xd7\x0b\xff\xe3\x04\xe0\xa4\xf2`\x83\x08\xd7\x18 \xd3\x1f\xd3\x1f\xd3\x1f\xf8#\x13\xbb\xf2c\xedD\xd0\xd3\x1f\xd3\x1f\xd3\xff\xd1Q2\xba\xf2\xa1QD\xba\xf2\xa2\x04\xf9\x01T\x10U\xf9\x10\xf2\xa3\xf8\x00\x93 \xd7J\x96\xd3\x07\xd4\x02\xfb\x00\xe8\xd1\x01\xa4\xc8\xcb\x1f\xcb\x1f\xcb\xff\xc9\xedT\x10\xbdm\xad') 15 | WALLET_V3_R1_CODE = Cell.one_from_boc( 16 | b'\xb5\xee\x9crA\x01\x01\x01\x00b\x00\x00\xc0\xff\x00 \xdd \x82\x01L\x97\xba\x970\xedD\xd0\xd7\x0b\x1f\xe0\xa4\xf2`\x83\x08\xd7\x18 \xd3\x1f\xd3\x1f\xd3\x1f\xf8#\x13\xbb\xf2c\xedD\xd0\xd3\x1f\xd3\x1f\xd3\xff\xd1Q2\xba\xf2\xa1QD\xba\xf2\xa2\x04\xf9\x01T\x10U\xf9\x10\xf2\xa3\xf8\x00\x93 \xd7J\x96\xd3\x07\xd4\x02\xfb\x00\xe8\xd1\x01\xa4\xc8\xcb\x1f\xcb\x1f\xcb\xff\xc9\xedT?\xben\xe0') 17 | WALLET_V4_R2_CODE = Cell.one_from_boc( 18 | b'\xb5\xee\x9crA\x02\x14\x01\x00\x02\xd4\x00\x01\x14\xff\x00\xf4\xa4\x13\xf4\xbc\xf2\xc8\x0b\x01\x02\x01 \x02\x03\x02\x01H\x04\x05\x04\xf8\xf2\x83\x08\xd7\x18 \xd3\x1f\xd3\x1f\xd3\x1f\x02\xf8#\xbb\xf2d\xedD\xd0\xd3\x1f\xd3\x1f\xd3\xff\xf4\x04\xd1QC\xba\xf2\xa1QQ\xba\xf2\xa2\x05\xf9\x01T\x10d\xf9\x10\xf2\xa3\xf8\x00$\xa4\xc8\xcb\x1fR@\xcb\x1fR0\xcb\xffR\x10\xf4\x00\xc9\xedT\xf8\x0f\x01\xd3\x07!\xc0\x00\x9flQ\x93 \xd7J\x96\xd3\x07\xd4\x02\xfb\x00\xe80\xe0!\xc0\x01\xe3\x00!\xc0\x02\xe3\x00\x01\xc0\x03\x910\xe3\r\x03\xa4\xc8\xcb\x1f\x12\xcb\x1f\xcb\xff\x10\x11\x12\x13\x02\xe6\xd0\x01\xd0\xd3\x03!q\xb0\x92_\x04\xe0"\xd7I\xc1 \x92_\x04\xe0\x02\xd3\x1f!\x82\x10plug\xbd"\x82\x10dstr\xbd\xb0\x92_\x05\xe0\x03\xfa@0 \xfaD\x01\xc8\xca\x07\xcb\xff\xc9\xd0\xedD\xd0\x81\x01@\xd7!\xf4\x040\\\x81\x01\x08\xf4\no\xa11\xb3\x92_\x07\xe0\x05\xd3?\xc8%\x82\x10plug\xba\x9280\xe3\r\x03\x82\x10dstr\xba\x92_\x06\xe3\r\x06\x07\x02\x01 \x08\t\x00x\x01\xfa\x00\xf4\x040\xf8\'o"0P\n\xa1!\xbe\xf2\xe0P\x82\x10plug\x83\x1e\xb1p\x80\x18P\x04\xcb\x05&\xcf\x16X\xfa\x02\x19\xf4\x00\xcbi\x17\xcb\x1fR`\xcb? \xc9\x80@\xfb\x00\x06\x00\x8aP\x04\x81\x01\x08\xf4Y0\xedD\xd0\x81\x01@\xd7 \xc8\x01\xcf\x16\xf4\x00\xc9\xedT\x01r\xb0\x8e#\x82\x10dstr\x83\x1e\xb1p\x80\x18P\x05\xcb\x05P\x03\xcf\x16#\xfa\x02\x13\xcbj\xcb\x1f\xcb?\xc9\x80@\xfb\x00\x92_\x03\xe2\x02\x01 \n\x0b\x00Y\xbd$+oj&\x84\x08\n\x06\xb9\x0f\xa0!\x84p\xd4\x08\x08G\xa4\x93})\x91\x0c\xe6\x90>\x9f\xf9\x83x\x12\x80\x1bx\x10\x14\x89\x87\x15\x9f1\x84\x02\x01X\x0c\r\x00\x11\xb8\xc9~\xd4M\rp\xb1\xf8\x00=\xb2\x9d\xfbQ4 @P5\xc8}\x01\x0c\x00\xb22\x81\xf2\xff\xf2t\x00`@B=\x02\x9b\xe8L`\x02\x01 \x0e\x0f\x00\x19\xad\xcev\xa2h@ k\x90\xeb\x85\xff\xc0\x00\x19\xaf\x1d\xf6\xa2h@\x10k\x90\xeb\x85\x8f\xc0\x00n\xd2\x07\xfa\x00\xd4\xd4"\xf9\x00\x05\xc8\xca\x07\x15\xcb\xff\xc9\xd0wt\x80\x18\xc8\xcb\x05\xcb\x02"\xcf\x16P\x05\xfa\x02\x14\xcbk\x12\xcc\xcc\xc9s\xfb\x00\xc8@\x14\x81\x01\x08\xf4Q\xf2\xa7\x02\x00p\x81\x01\x08\xd7\x18\xfa\x00\xd3?\xc8T G\x81\x01\x08\xf4Q\xf2\xa7\x82\x10notept\x80\x18\xc8\xcb\x05\xcb\x02P\x06\xcf\x16P\x04\xfa\x02\x14\xcbj\x12\xcb\x1f\xcb?\xc9s\xfb\x00\x02\x00l\x81\x01\x08\xd7\x18\xfa\x00\xd3?0R$\x81\x01\x08\xf4Y\xf2\xa7\x82\x10dstrpt\x80\x18\xc8\xcb\x05\xcb\x02P\x05\xcf\x16P\x03\xfa\x02\x13\xcbj\xcb\x1f\x12\xcb?\xc9s\xfb\x00\x00\n\xf4\x00\xc9\xedTib%\xe5') 19 | 20 | 21 | class WalletError(ContractError): 22 | pass 23 | 24 | 25 | class Wallet(Contract): 26 | @classmethod 27 | async def from_private_key(cls, *args, **kwargs): ... 28 | 29 | @staticmethod 30 | def raw_create_transfer_msg(*args, **kwargs): ... 31 | 32 | @classmethod 33 | async def from_mnemonic(cls, *args, **kwargs): ... 34 | 35 | @classmethod 36 | async def create(cls, *args, **kwargs): ... 37 | 38 | @staticmethod 39 | def create_wallet_internal_message(destination: Address, send_mode: int = 3, value: int = 0, body: typing.Union[Cell, str] = None, 40 | state_init: typing.Optional[StateInit] = None, **kwargs) -> WalletMessage: 41 | if isinstance(body, str): 42 | body = Builder()\ 43 | .store_uint(0, 32)\ 44 | .store_snake_string(body)\ 45 | .end_cell() 46 | 47 | message = Contract.create_internal_msg(dest=destination, value=value, body=body, state_init=state_init, **kwargs) 48 | return WalletMessage(send_mode=send_mode, message=message) 49 | 50 | async def send_init_external(self): ... 51 | 52 | async def raw_transfer(self, *args, **kwargs): ... 53 | 54 | async def transfer(self, *args, **kwargs): ... 55 | 56 | 57 | class BaseWallet(Wallet): 58 | """ 59 | class for user wallets such as v4r2, v3r2, etc. 60 | """ 61 | 62 | VERSION = None 63 | 64 | @classmethod 65 | async def from_private_key(cls, provider: LiteClientLike, private_key: bytes, wc: int = 0, 66 | wallet_id: typing.Optional[int] = None, version: str = 'v3r2'): 67 | public_key = private_key_to_public_key(private_key) 68 | if version == 'v3r2': 69 | return await WalletV3R2.from_data(provider=provider, wc=wc, public_key=public_key, wallet_id=wallet_id, 70 | private_key=private_key) 71 | elif version == 'v4r2': 72 | return await WalletV4R2.from_data(provider=provider, wc=wc, public_key=public_key, wallet_id=wallet_id, 73 | private_key=private_key) 74 | elif version == 'v3r1': 75 | return await WalletV3R1.from_data(provider=provider, wc=wc, public_key=public_key, wallet_id=wallet_id, 76 | private_key=private_key) 77 | else: 78 | raise Exception(f'Wallet version {version} does not supported') 79 | 80 | @classmethod 81 | async def from_mnemonic(cls, provider: LiteClientLike, mnemonics: typing.Union[list, str], wc: int = 0, 82 | wallet_id: typing.Optional[int] = None, version: str = 'v3r2'): 83 | version = cls.VERSION or version 84 | if isinstance(mnemonics, str): 85 | mnemonics = mnemonics.split() 86 | assert mnemonic_is_valid(mnemonics), 'mnemonics are invalid!' 87 | _, private_key = mnemonic_to_private_key(mnemonics) 88 | return await cls.from_private_key(provider, private_key, wc, wallet_id, version) 89 | 90 | @classmethod 91 | async def create(cls, provider: LiteClientLike, wc: int = 0, wallet_id: typing.Optional[int] = None, 92 | version: str = 'v3r2'): 93 | """ 94 | :param provider: provider 95 | :param wc: wallet workchain 96 | :param wallet_id: subwallet_id 97 | :param version: wallet version 98 | :return: mnemonics and Wallet instance of provided version 99 | """ 100 | version = cls.VERSION or version 101 | mnemo = mnemonic_new(24) 102 | return mnemo, await cls.from_mnemonic(provider, mnemo, wc, wallet_id, version) 103 | 104 | @staticmethod 105 | def raw_create_transfer_msg(private_key: bytes, seqno: int, wallet_id: int, messages: typing.List[WalletMessage], 106 | valid_until: typing.Optional[int] = None) -> Cell: 107 | signing_message = Builder().store_uint(wallet_id, 32) 108 | if seqno == 0: 109 | signing_message.store_bits('1' * 32) # bin(2**32 - 1) 110 | else: 111 | if valid_until is not None: 112 | signing_message.store_uint(valid_until, 32) 113 | else: 114 | signing_message.store_uint(int(time.time()) + 60, 32) 115 | signing_message.store_uint(seqno, 32) 116 | for m in messages: 117 | signing_message.store_cell(m.serialize()) 118 | signing_message = signing_message.end_cell() 119 | signature = sign_message(signing_message.hash, private_key) 120 | return Builder() \ 121 | .store_bytes(signature) \ 122 | .store_cell(signing_message) \ 123 | .end_cell() 124 | 125 | async def raw_transfer(self, msgs: typing.List[WalletMessage], seqno_from_get_meth: bool = True): 126 | """ 127 | :param msgs: list of WalletMessages. to create one call create_wallet_internal_message meth 128 | :param seqno_from_get_meth: if True LiteClient will request seqno get method and use it, otherwise seqno from contract data will be taken 129 | """ 130 | assert len(msgs) <= 4, 'for common wallet maximum messages amount is 4' 131 | if 'private_key' not in self.__dict__: 132 | raise WalletError('must specify wallet private key!') 133 | 134 | if seqno_from_get_meth: 135 | seqno = await self.get_seqno() 136 | else: 137 | seqno = self.seqno 138 | transfer_msg = self.raw_create_transfer_msg(private_key=self.private_key, seqno=seqno, wallet_id=self.wallet_id, messages=msgs) 139 | 140 | return await self.send_external(body=transfer_msg) 141 | 142 | async def transfer(self, destination: typing.Union[Address, str], amount: int, body: Cell = Cell.empty(), 143 | state_init: StateInit = None): 144 | if isinstance(destination, str): 145 | destination = Address(destination) 146 | wallet_message = self.create_wallet_internal_message(destination=destination, value=amount, body=body, state_init=state_init) 147 | return await self.raw_transfer(msgs=[wallet_message]) 148 | 149 | async def send_init_external(self): 150 | if not self.state_init: 151 | raise ContractError('contract does not have state_init attribute') 152 | if 'private_key' not in self.__dict__: 153 | raise WalletError('must specify wallet private key!') 154 | body = self.raw_create_transfer_msg(private_key=self.private_key, seqno=0, wallet_id=self.wallet_id, messages=[]) 155 | return await self.send_external(state_init=self.state_init, body=body) 156 | 157 | async def get_seqno(self) -> int: 158 | """ 159 | :return: seqno from wallet's get method 160 | """ 161 | return (await super().run_get_method('seqno'))[0] 162 | 163 | async def get_public_key(self) -> int: 164 | """ 165 | :return: public key from wallet's get method 166 | """ 167 | if self.__class__ == WalletV3R1: 168 | raise Exception('WalletV3R1 doesn\'t have get_public_key get method. Use .public_key attribute') 169 | return (await super().run_get_method('get_public_key'))[0] 170 | 171 | async def deploy_via_internal(self, contract: Contract, deploy_amount: int = int(0.05 * 10**9)): 172 | return await self.transfer(destination=contract.address, amount=deploy_amount, state_init=contract.state_init) 173 | 174 | 175 | class WalletV3(BaseWallet): 176 | 177 | @classmethod 178 | async def from_code_and_data(cls, provider: LiteClientLike, code: Cell, public_key: bytes, wc: int = 0, 179 | wallet_id: typing.Optional[int] = None, private_key: typing.Optional[bytes] = None): 180 | data = cls.create_data_cell(public_key, wallet_id, wc) 181 | return await super().from_code_and_data(provider, wc, code, data, private_key=private_key) 182 | 183 | @staticmethod 184 | def create_data_cell(public_key: bytes, wallet_id: typing.Optional[int] = None, 185 | wc: typing.Optional[int] = 0) -> Cell: 186 | if wallet_id is None: 187 | wallet_id = 698983191 + wc 188 | return WalletV3Data(seqno=0, wallet_id=wallet_id, public_key=public_key).serialize() 189 | 190 | @property 191 | def seqno(self) -> int: 192 | """ 193 | :return: seqno taken from contract data 194 | """ 195 | return WalletV3Data.deserialize(self.state.data.begin_parse()).seqno 196 | 197 | @property 198 | def wallet_id(self) -> int: 199 | """ 200 | :return: wallet_id taken from contract data 201 | """ 202 | return WalletV3Data.deserialize(self.state.data.begin_parse()).wallet_id 203 | 204 | @property 205 | def public_key(self) -> bytes: 206 | """ 207 | :return: public_key taken from contract data 208 | """ 209 | return WalletV3Data.deserialize(self.state.data.begin_parse()).public_key 210 | 211 | 212 | class WalletV4(BaseWallet): 213 | 214 | @classmethod 215 | async def from_code_and_data(cls, provider: LiteClientLike, code: Cell, public_key: bytes, wc: int = 0, 216 | wallet_id: typing.Optional[int] = None, **kwargs): 217 | data = cls.create_data_cell(public_key, wallet_id, wc) 218 | return await super().from_code_and_data(provider, wc, code, data, **kwargs) 219 | 220 | @staticmethod 221 | def create_data_cell(public_key: bytes, wallet_id: typing.Optional[int] = None, wc: typing.Optional[int] = 0, 222 | plugins: typing.Optional[Cell] = None) -> Cell: 223 | if wallet_id is None: 224 | wallet_id = 698983191 + wc 225 | return WalletV4Data(seqno=0, wallet_id=wallet_id, public_key=public_key, plugins=plugins).serialize() 226 | 227 | @staticmethod 228 | def raw_create_transfer_msg(private_key: bytes, seqno: int, wallet_id: int, messages: typing.List[WalletMessage], 229 | valid_until: typing.Optional[int] = None, op_code: int = 0) -> Cell: 230 | signing_message = Builder().store_uint(wallet_id, 32) 231 | if seqno == 0: 232 | signing_message.store_bits('1' * 32) # bin(2**32 - 1) 233 | else: 234 | if valid_until is not None: 235 | signing_message.store_uint(valid_until, 32) 236 | else: 237 | signing_message.store_uint(int(time.time()) + 60, 32) 238 | signing_message.store_uint(seqno, 32) 239 | signing_message.store_uint(op_code, 8) 240 | for m in messages: 241 | signing_message.store_cell(m.serialize()) 242 | signing_message = signing_message.end_cell() 243 | signature = sign_message(signing_message.hash, private_key) 244 | return Builder() \ 245 | .store_bytes(signature) \ 246 | .store_cell(signing_message) \ 247 | .end_cell() 248 | 249 | @property 250 | def seqno(self) -> int: 251 | """ 252 | :return: seqno taken from contract data 253 | """ 254 | return WalletV4Data.deserialize(self.state.data.begin_parse()).seqno 255 | 256 | @property 257 | def wallet_id(self) -> int: 258 | """ 259 | :return: wallet_id taken from contract data 260 | """ 261 | return WalletV4Data.deserialize(self.state.data.begin_parse()).wallet_id 262 | 263 | @property 264 | def public_key(self) -> bytes: 265 | """ 266 | :return: public_key taken from contract data 267 | """ 268 | return WalletV4Data.deserialize(self.state.data.begin_parse()).public_key 269 | 270 | @property 271 | def plugins(self) -> Cell: 272 | return WalletV4Data.deserialize(self.state.data.begin_parse()).plugins 273 | 274 | async def get_plugin_list(self): 275 | """ 276 | :return: plugins list from wallet's get method 277 | """ 278 | return (await super().run_get_method(method='get_plugin_list', stack=[]))[0] 279 | 280 | async def is_plugin_installed(self, address: Address) -> bool: 281 | """ 282 | :return: is plugin installed from wallet's get method 283 | """ 284 | return bool((await super().run_get_method(method='is_plugin_installed', stack=[address.wc, address.hash_part]))[0]) 285 | 286 | 287 | class WalletV3R1(WalletV3): 288 | 289 | VERSION = 'v3r1' 290 | 291 | @classmethod 292 | async def from_data(cls, provider: LiteClientLike, public_key: bytes, wc: int = 0, 293 | wallet_id: typing.Optional[int] = None, **kwargs): 294 | return await super().from_code_and_data(provider=provider, code=WALLET_V3_R1_CODE, public_key=public_key, wc=wc, 295 | wallet_id=wallet_id, **kwargs) 296 | 297 | 298 | class WalletV3R2(WalletV3): 299 | 300 | VERSION = 'v3r2' 301 | 302 | @classmethod 303 | async def from_data(cls, provider: LiteClientLike, public_key: bytes, wc: int = 0, 304 | wallet_id: typing.Optional[int] = None, **kwargs): 305 | return await super().from_code_and_data(provider=provider, code=WALLET_V3_R2_CODE, public_key=public_key, wc=wc, 306 | wallet_id=wallet_id, **kwargs) 307 | 308 | 309 | class WalletV4R2(WalletV4): 310 | 311 | VERSION = 'v4r2' 312 | 313 | @classmethod 314 | async def from_data(cls, provider: LiteClientLike, public_key: bytes, wc: int = 0, 315 | wallet_id: typing.Optional[int] = None, **kwargs): 316 | return await super().from_code_and_data(provider=provider, code=WALLET_V4_R2_CODE, public_key=public_key, wc=wc, 317 | wallet_id=wallet_id, **kwargs) 318 | -------------------------------------------------------------------------------- /pytoniq/liteclient/__init__.py: -------------------------------------------------------------------------------- 1 | import typing 2 | 3 | from .client import LiteClient, LiteClientError, RunGetMethodError, BlockId, BlockIdExt, LiteServerError 4 | from .balancer import LiteBalancer, BalancerError 5 | 6 | LiteClientLike = typing.Union[LiteClient, LiteBalancer] 7 | -------------------------------------------------------------------------------- /pytoniq/liteclient/_balancer_codegen.py: -------------------------------------------------------------------------------- 1 | codegen_text1 = ''' """CODE BELOW IS AUTOGENERATED. DO NOT EDIT MANUALLY"""\n\n''' 2 | codegen_text2 = ''' """CODE ABOVE IS AUTOGENERATED. DO NOT EDIT MANUALLY"""\n\n''' 3 | 4 | 5 | exceptions = { 6 | 'raw_send_message' 7 | } 8 | 9 | 10 | def main(): 11 | 12 | with open('client.py', 'r') as f: 13 | offset = False 14 | lines = f.readlines() 15 | 16 | result = codegen_text1 17 | tmp = '' 18 | 19 | for line in lines: 20 | if 'async def get_masterchain_info(self):' in line: 21 | offset = True 22 | if not offset: 23 | continue 24 | if 'async def' in line: 25 | name = line[line.index(' async def ') + 14: line.index('(')] 26 | if name in exceptions or name.startswith('_'): 27 | continue 28 | tmp += line 29 | if tmp and ':\n' in line: 30 | if name not in line: 31 | tmp += line 32 | tmp = tmp.replace('):', ', **kwargs):') 33 | tmp = tmp.replace(') ->', ', **kwargs) ->') 34 | tmp += ' ' * 2 + f"return await self.execute_method('{name}', **self._get_args(locals())) \n\n" 35 | result += tmp 36 | tmp = '' 37 | elif tmp: 38 | if name not in line: 39 | tmp += line 40 | 41 | result += codegen_text2 42 | 43 | with open('balancer.py', 'r') as f: 44 | text = f.read() 45 | new_text = text[:text.index(codegen_text1)] + result + text[text.index(codegen_text2) + len(codegen_text2):] 46 | 47 | with open('balancer.py', 'w') as f: 48 | f.write(new_text) 49 | 50 | 51 | if __name__ == '__main__': 52 | main() 53 | -------------------------------------------------------------------------------- /pytoniq/liteclient/balancer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import random 4 | import time 5 | import typing 6 | 7 | import requests 8 | from pytoniq_core import BlockIdExt, Block, Address, Account, ShardAccount, SimpleAccount, ShardDescr, Transaction, Cell 9 | from pytoniq_core.tlb.block import BinTree 10 | 11 | from .client import LiteClient, LiteClientError, LiteServerError 12 | 13 | 14 | class BalancerError(LiteClientError): 15 | pass 16 | 17 | 18 | class LiteBalancer: 19 | 20 | def __init__(self, peers: typing.List[LiteClient], timeout: int = 10): 21 | 22 | self._peers = peers 23 | self._alive_peers: typing.Set[int] = set() 24 | self._archival_peers = set() 25 | 26 | self._checker: asyncio.Task = None 27 | 28 | self._mc_blocks = {} # {index: masterchain_seqno} 29 | self._av_resp_time = {} # {index: average_response_time} 30 | self._total_req_num = {} # {index: successful_requests_num} 31 | self._current_req_num = {} # {index: current_waiting_requests_num} 32 | 33 | self._logger = logging.getLogger(self.__class__.__name__) 34 | 35 | self.inited = False 36 | self.max_req_per_peer = 100 37 | self.max_retries = 1 38 | self.timeout = timeout 39 | 40 | @property 41 | def peers_num(self): 42 | return len(self._peers) 43 | 44 | @property 45 | def alive_peers_num(self): 46 | return len(self._alive_peers) 47 | 48 | @property 49 | def archival_peers_num(self): 50 | return len(self._archival_peers) 51 | 52 | @property 53 | def last_mc_block(self): 54 | seqno = self._find_consensus_block() 55 | for p in self._peers: 56 | if p.last_mc_block is not None and p.last_mc_block.seqno == seqno: 57 | return p.last_mc_block 58 | return None 59 | 60 | def set_max_retries(self, retries_num: int) -> None: 61 | self.max_retries = retries_num 62 | 63 | async def start_up(self): 64 | have_blockstore = False 65 | tasks = [] 66 | for client in self._peers: 67 | if client.trust_level >= 1: 68 | tasks.append(self._connect_to_peer(client)) 69 | elif have_blockstore: 70 | tasks.append(self._connect_to_peer(client)) 71 | else: # so we can verify blocks proof link only once 72 | connected = await self._connect_to_peer(client) 73 | if connected: 74 | have_blockstore = True 75 | 76 | async def f(): return connected 77 | tasks.append(f()) 78 | result = await asyncio.gather(*tasks) 79 | for i, client in enumerate(self._peers): 80 | if result[i]: 81 | self._alive_peers.add(i) 82 | await self._find_archives() 83 | self._checker = asyncio.create_task(self._check_peers()) 84 | self._delete_unsync_peers() 85 | self.inited = True 86 | 87 | async def _find_archives(self): 88 | tasks = [] 89 | inds = [] 90 | for i in self._alive_peers: 91 | tasks.append(self.check_archive(self._peers[i])) 92 | inds.append(i) 93 | 94 | result = await asyncio.gather(*tasks, return_exceptions=True) 95 | for i, r in enumerate(result): 96 | if isinstance(r, bool): 97 | if r: 98 | self._archival_peers.add(inds[i]) 99 | else: 100 | self._archival_peers.discard(inds[i]) # almost impossible case when peer becomes unarchival 101 | elif isinstance(r, Exception): 102 | self._logger.info(f'Failed to check peer {inds[i]} on archival: {r}') 103 | 104 | @staticmethod 105 | async def check_archive(peer: LiteClient): 106 | try: 107 | blk, _ = await peer.lookup_block(wc=-1, shard=-2**63, seqno=random.randint(1, 1024)) # as ton-http-api does, but maybe need to ask the first block. todo 108 | return True 109 | except LiteClientError: 110 | return False 111 | 112 | async def _connect_to_peer(self, client: LiteClient): 113 | self._check_errors(client) 114 | if client.listener is not None and not client.listener.done(): 115 | client.listener.cancel() 116 | while not client.listener.done(): 117 | await asyncio.sleep(0) 118 | try: 119 | if client.trust_level >= 1: 120 | await asyncio.wait_for(client.connect(), 3) 121 | else: 122 | await client.connect() 123 | return True 124 | except asyncio.TimeoutError: 125 | return False 126 | except Exception as e: 127 | self._logger.debug(f'Failed to connect to the peer {client.server.get_key_id().hex()}: {e}') 128 | return False 129 | finally: 130 | self._check_errors(client) 131 | 132 | async def _ping_peer(self, peer: LiteClient): 133 | try: 134 | await asyncio.wait_for(peer.get_masterchain_info(), 3) 135 | return True 136 | except asyncio.TimeoutError: 137 | return False 138 | except Exception as e: 139 | self._logger.debug(f'Failed to ping peer {peer.server.get_key_id().hex()}: {e}') 140 | 141 | def _check_errors(self, client: LiteClient): 142 | for task in [client.updater, client.pinger, client.listener]: 143 | if task is not None and task.done(): 144 | if not task.cancelled(): 145 | self._logger.debug(f'client task {task} failed with exception: {task.exception()}') 146 | return True 147 | return False 148 | 149 | async def _check_peers(self): 150 | while True: 151 | await asyncio.sleep(3) 152 | for i, client in enumerate(self._peers): 153 | self._delete_unsync_peers() 154 | client: LiteClient 155 | if client.inited: 156 | if self._check_errors(client): 157 | self._alive_peers.discard(i) 158 | await client.close() 159 | if await self._connect_to_peer(client): 160 | self._alive_peers.add(i) 161 | else: 162 | self._alive_peers.discard(i) 163 | continue 164 | ping_res = await self._ping_peer(client) 165 | if ping_res: 166 | self._alive_peers.add(i) 167 | else: 168 | self._alive_peers.discard(i) 169 | else: 170 | if await self._connect_to_peer(client): 171 | self._alive_peers.add(i) 172 | else: 173 | self._alive_peers.discard(i) 174 | 175 | async def connect(self): 176 | raise BalancerError(f'Use start_up()') 177 | 178 | def _build_priority_list(self, only_archive: bool = False): 179 | sorted_peers = sorted( 180 | list(self._alive_peers) if not only_archive else list(self._archival_peers), 181 | key=lambda e: (self._mc_blocks.get(e, 0), -self._av_resp_time.get(e, self.timeout * 1000)), 182 | reverse=True 183 | ) # first peers are with biggest masterchain seqno and lowest avg time response 184 | return sorted_peers 185 | 186 | def _choose_peer(self, only_archive: bool = False): 187 | peers = self._build_priority_list(only_archive) 188 | min_req = float('inf') 189 | for p in peers: 190 | peer_req = self._current_req_num.get(p, 0) 191 | if peer_req <= self.max_req_per_peer: 192 | return p 193 | if peer_req < min_req: 194 | min_req = peer_req 195 | for p in peers: 196 | peer_req = self._current_req_num.get(p, 0) 197 | if peer_req <= min_req: 198 | return p 199 | return peers[0] # should never happen 200 | 201 | @staticmethod 202 | def _calc_new_average(old_average: int, n: int, new_value: int): 203 | return (old_average * (n - 1) + new_value) / n 204 | 205 | def _update_average_request_time(self, ls_index: int, req_time: int): 206 | old = self._av_resp_time.get(ls_index, 0) 207 | req_num = self._total_req_num.get(ls_index, 0) 208 | req_num += 1 209 | self._av_resp_time[ls_index] = self._calc_new_average(old, req_num, req_time) 210 | self._total_req_num[ls_index] = req_num 211 | 212 | def _update_mc_seqno(self, ls_index: int): 213 | client = self._peers[ls_index] 214 | blk = client.last_mc_block 215 | if blk and self._mc_blocks.get(ls_index, 0) < blk.seqno: 216 | self._mc_blocks[ls_index] = blk.seqno 217 | 218 | def _update_mc_seqnos(self): 219 | for i in range(len(self._peers)): 220 | self._update_mc_seqno(i) 221 | 222 | def _find_consensus_block(self): 223 | self._update_mc_seqnos() 224 | seqnos = sorted(self._mc_blocks.values(), reverse=True) 225 | if not seqnos: 226 | return 0 227 | return seqnos[len(seqnos) * 2 // 3] # block that knows at least 2/3 liteservers 228 | 229 | def _delete_unsync_peers(self): 230 | cons_block = self._find_consensus_block() 231 | for i in list(self._alive_peers): 232 | if self._mc_blocks.get(i, 0) < cons_block: 233 | self._alive_peers.discard(i) 234 | 235 | async def execute_method(self, method_name_: str, *args, **kwargs) -> typing.Union[dict, typing.Any]: 236 | only_archive = kwargs.pop('only_archive', False) 237 | choose_random = kwargs.pop('choose_random', False) 238 | retry = False 239 | i = 0 240 | while i < self.max_retries or retry: 241 | retry = False 242 | 243 | if not len(self._alive_peers): 244 | raise BalancerError(f'have no alive peers') 245 | 246 | if only_archive and choose_random: 247 | raise BalancerError('Currently you cant execute method for both random and archive peer') 248 | 249 | if only_archive and not len(self._archival_peers): 250 | await self._find_archives() # give one more chance to find 251 | if not len(self._archival_peers): 252 | raise BalancerError(f'have no alive archive peers') 253 | 254 | self._update_mc_seqnos() 255 | if choose_random: 256 | ind = random.choice(list(self._alive_peers)) 257 | else: 258 | ind = self._choose_peer(only_archive) 259 | peer: LiteClient = self._peers[ind] 260 | 261 | peer_meth = getattr(peer, method_name_, None) 262 | self._current_req_num[ind] = self._current_req_num.get(ind, 0) + 1 263 | s = time.time_ns() 264 | if not peer_meth: 265 | raise BalancerError('Unknown method for peer') 266 | try: 267 | resp = await peer_meth(*args, **kwargs) 268 | self._update_average_request_time(ind, (time.time_ns() - s) // 10**6) # provide milliseconds 269 | return resp 270 | except asyncio.TimeoutError: 271 | self._update_average_request_time(ind, self.timeout * 10**6) # provide milliseconds 272 | self._alive_peers.discard(ind) 273 | continue 274 | except LiteServerError as e: 275 | if e.message == 'timeout': 276 | self._update_average_request_time(ind, self.timeout * 10 ** 6) 277 | self._alive_peers.discard(ind) 278 | continue 279 | raise e 280 | except ConnectionError: # if socket is dead we just try another peer and somewhere in future will reconnect to this one 281 | self._alive_peers.discard(ind) 282 | retry = True 283 | continue 284 | finally: 285 | self._current_req_num[ind] -= 1 286 | raise asyncio.TimeoutError() 287 | 288 | @staticmethod 289 | def _get_args(locals_: dict): 290 | a = locals_.copy() 291 | for k in list(a.keys()): 292 | if k.startswith('_'): 293 | a.pop(k) 294 | a.pop('self') 295 | kwargs = a.pop('kwargs', {}) 296 | a |= kwargs 297 | return a 298 | 299 | """CODE BELOW IS AUTOGENERATED. DO NOT EDIT MANUALLY""" 300 | 301 | async def get_masterchain_info(self, **kwargs): 302 | return await self.execute_method('get_masterchain_info', **self._get_args(locals())) 303 | 304 | async def raw_wait_masterchain_seqno(self, seqno: int, timeout_ms: int, suffix: bytes = b'', **kwargs): 305 | return await self.execute_method('raw_wait_masterchain_seqno', **self._get_args(locals())) 306 | 307 | async def wait_masterchain_seqno(self, seqno: int, timeout_ms: int, schema_name: str, data: dict = None, **kwargs): 308 | return await self.execute_method('wait_masterchain_seqno', **self._get_args(locals())) 309 | 310 | async def get_masterchain_info_ext(self, **kwargs): 311 | return await self.execute_method('get_masterchain_info_ext', **self._get_args(locals())) 312 | 313 | async def get_time(self, **kwargs): 314 | return await self.execute_method('get_time', **self._get_args(locals())) 315 | 316 | async def get_version(self, **kwargs): 317 | return await self.execute_method('get_version', **self._get_args(locals())) 318 | 319 | async def get_state(self, wc: int, shard: typing.Optional[int], 320 | seqno: int, root_hash: typing.Union[str, bytes], 321 | file_hash: typing.Union[str, bytes] 322 | , **kwargs) -> dict: 323 | return await self.execute_method('get_state', **self._get_args(locals())) 324 | 325 | async def raw_get_block_header(self, block: BlockIdExt, **kwargs) -> Block: 326 | return await self.execute_method('raw_get_block_header', **self._get_args(locals())) 327 | 328 | async def get_block_header(self, wc: int, shard: typing.Optional[int], seqno: int, 329 | root_hash: typing.Union[str, bytes], 330 | file_hash: typing.Union[str, bytes] 331 | , **kwargs) -> Block: 332 | return await self.execute_method('get_block_header', **self._get_args(locals())) 333 | 334 | async def lookup_block(self, wc: int, shard: int, seqno: int = -1, 335 | lt: typing.Optional[int] = None, 336 | utime: typing.Optional[int] = None, **kwargs) -> typing.Tuple[BlockIdExt, Block]: 337 | return await self.execute_method('lookup_block', **self._get_args(locals())) 338 | 339 | async def raw_get_block(self, block: BlockIdExt, **kwargs) -> Block: 340 | return await self.execute_method('raw_get_block', **self._get_args(locals())) 341 | 342 | async def get_block(self, wc: int, shard: typing.Optional[int], 343 | seqno: int, root_hash: typing.Union[str, bytes], 344 | file_hash: typing.Union[str, bytes], **kwargs) -> Block: 345 | return await self.execute_method('get_block', **self._get_args(locals())) 346 | 347 | async def raw_get_account_state(self, address: typing.Union[str, Address], 348 | block: typing.Optional[BlockIdExt] = None 349 | , **kwargs) -> typing.Tuple[typing.Optional[Account], typing.Optional[ShardAccount]]: 350 | return await self.execute_method('raw_get_account_state', **self._get_args(locals())) 351 | 352 | async def get_account_state(self, address: typing.Union[str, Address], **kwargs) -> SimpleAccount: 353 | return await self.execute_method('get_account_state', **self._get_args(locals())) 354 | 355 | async def run_get_method(self, address: typing.Union[Address, str], 356 | method: typing.Union[int, str], stack: list, 357 | block: BlockIdExt = None 358 | , **kwargs) -> list: 359 | return await self.execute_method('run_get_method', **self._get_args(locals())) 360 | 361 | async def run_get_method_remote(self, address: typing.Union[Address, str], 362 | method: typing.Union[int, str], stack: list, 363 | block: BlockIdExt = None 364 | , **kwargs) -> list: 365 | return await self.execute_method('run_get_method_remote', **self._get_args(locals())) 366 | 367 | async def run_get_method_local(self, address: typing.Union[Address, str], 368 | method: typing.Union[int, str], stack: list, 369 | block: BlockIdExt = None, gas_limit: int = 300000, **kwargs) -> list: 370 | return await self.execute_method('run_get_method_local', **self._get_args(locals())) 371 | 372 | async def raw_get_shard_info(self, block: typing.Optional[BlockIdExt] = None, 373 | wc: int = 0, shard: int = -9223372036854775808, 374 | exact: bool = True 375 | , **kwargs) -> ShardDescr: 376 | return await self.execute_method('raw_get_shard_info', **self._get_args(locals())) 377 | 378 | async def raw_get_all_shards_info(self, block: typing.Optional[BlockIdExt] = None, **kwargs) -> typing.Dict[int, BinTree]: 379 | return await self.execute_method('raw_get_all_shards_info', **self._get_args(locals())) 380 | 381 | async def get_all_shards_info(self, block: typing.Optional[BlockIdExt] = None, **kwargs) -> typing.List[BlockIdExt]: 382 | return await self.execute_method('get_all_shards_info', **self._get_args(locals())) 383 | 384 | async def get_one_transaction(self, address: typing.Union[Address, str], 385 | lt: int, block: BlockIdExt 386 | , **kwargs) -> typing.Optional[Transaction]: 387 | return await self.execute_method('get_one_transaction', **self._get_args(locals())) 388 | 389 | async def raw_get_transactions(self, address: typing.Union[Address, str], count: int, 390 | from_lt: int = None, from_hash: typing.Optional[bytes] = None 391 | , **kwargs) -> typing.Tuple[typing.List[Transaction], typing.List[BlockIdExt]]: 392 | return await self.execute_method('raw_get_transactions', **self._get_args(locals())) 393 | 394 | async def get_transactions(self, address: typing.Union[Address, str], count: int, 395 | from_lt: int = None, from_hash: typing.Optional[bytes] = None, 396 | to_lt: int = 0 397 | , **kwargs) -> typing.List[Transaction]: 398 | return await self.execute_method('get_transactions', **self._get_args(locals())) 399 | 400 | async def raw_get_block_transactions(self, block: BlockIdExt, count: int = 1024, **kwargs) -> typing.List[dict]: 401 | return await self.execute_method('raw_get_block_transactions', **self._get_args(locals())) 402 | 403 | async def raw_get_block_transactions_ext(self, block: BlockIdExt, count: int = 1024, **kwargs) -> typing.List[Transaction]: 404 | return await self.execute_method('raw_get_block_transactions_ext', **self._get_args(locals())) 405 | 406 | async def raw_get_mc_block_proof(self, known_block: BlockIdExt, target_block: typing.Optional[BlockIdExt] = None, 407 | return_best_key_block=False 408 | , **kwargs) -> typing.Tuple[ 409 | bool, 410 | BlockIdExt, 411 | typing.Optional[BlockIdExt], 412 | typing.Optional[int] 413 | ]: 414 | return await self.execute_method('raw_get_mc_block_proof', **self._get_args(locals())) 415 | 416 | async def get_mc_block_proof(self, known_block: BlockIdExt, 417 | target_block: BlockIdExt, 418 | return_best_key_block=False 419 | , **kwargs) -> typing.Tuple[typing.Optional[BlockIdExt], int]: 420 | return await self.execute_method('get_mc_block_proof', **self._get_args(locals())) 421 | 422 | async def prove_block(self, target_block: BlockIdExt, **kwargs) -> None: 423 | return await self.execute_method('prove_block', **self._get_args(locals())) 424 | 425 | async def get_config_all(self, blk: typing.Optional[BlockIdExt] = None, **kwargs) -> dict: 426 | return await self.execute_method('get_config_all', **self._get_args(locals())) 427 | 428 | async def get_config_params(self, params: typing.List[int], blk: typing.Optional[BlockIdExt] = None, **kwargs) -> dict: 429 | return await self.execute_method('get_config_params', **self._get_args(locals())) 430 | 431 | async def get_libraries(self, library_list: typing.List[typing.Union[bytes, str]], **kwargs) -> typing.Dict[str, typing.Optional[Cell]]: 432 | return await self.execute_method('get_libraries', **self._get_args(locals())) 433 | 434 | async def get_out_msg_queue_sizes(self, wc: int = None, shard: int = None, **kwargs): 435 | return await self.execute_method('get_out_msg_queue_sizes', **self._get_args(locals())) 436 | 437 | async def nonfinal_get_validator_groups(self, wc: int = None, shard: int = None, **kwargs): 438 | return await self.execute_method('nonfinal_get_validator_groups', **self._get_args(locals())) 439 | 440 | async def nonfinal_raw_get_candidate(self, candidate_id: dict, **kwargs): 441 | return await self.execute_method('nonfinal_raw_get_candidate', **self._get_args(locals())) 442 | 443 | async def nonfinal_get_candidate(self, candidate_id: dict, **kwargs): 444 | return await self.execute_method('nonfinal_get_candidate', **self._get_args(locals())) 445 | 446 | async def get_shard_block_proof(self, blk: BlockIdExt, prove_mc: bool = False, **kwargs): 447 | return await self.execute_method('get_shard_block_proof', **self._get_args(locals())) 448 | 449 | """CODE ABOVE IS AUTOGENERATED. DO NOT EDIT MANUALLY""" 450 | 451 | async def raw_send_message(self, message: bytes, **kwargs): 452 | _tasks = [] 453 | _choose_random = kwargs.pop('choose_random', True) 454 | _k = 4 if len(self._alive_peers) < 12 else len(self._alive_peers) // 3 455 | for _ in range(_k): # distribute external to approximately 1/3 alive peers, but not less than 4 456 | _tasks.append(self.execute_method('raw_send_message', choose_random=_choose_random, **self._get_args(locals()))) 457 | result = await asyncio.gather(*_tasks, return_exceptions=True) 458 | success = False 459 | exc = None 460 | for r in result: 461 | if isinstance(r, Exception): 462 | exc = r 463 | elif r == 1: 464 | success = True 465 | break 466 | if success: 467 | return 1 468 | raise exc # raise last exception 469 | 470 | async def close_all(self): 471 | for peer in self._peers: 472 | self._check_errors(peer) 473 | if peer.inited: 474 | await peer.close() 475 | self._checker.cancel() 476 | while not self._checker.done(): 477 | await asyncio.sleep(0) 478 | self.inited = False 479 | 480 | async def close(self): 481 | raise BalancerError('Use .close_all()') 482 | 483 | @classmethod 484 | def from_config(cls, config: dict, trust_level: int = 2, timeout: int = 10): 485 | clients = [] 486 | for i in range(len(config['liteservers'])): 487 | clients.append(LiteClient.from_config(config, i, trust_level, timeout)) 488 | return cls(clients) 489 | 490 | @classmethod 491 | def from_mainnet_config(cls, trust_level: int = 0, timeout: int = 10): 492 | config = requests.get('https://ton.org/global-config.json').json() 493 | return cls.from_config(config, trust_level, timeout) 494 | 495 | @classmethod 496 | def from_testnet_config(cls, trust_level: int = 0, timeout: int = 10): 497 | config = requests.get('https://ton.org/testnet-global.config.json').json() 498 | return cls.from_config(config, trust_level, timeout) 499 | 500 | async def __aenter__(self): 501 | await self.start_up() 502 | return self 503 | 504 | async def __aexit__(self, exc_type, exc_val, exc_tb): 505 | await self.close_all() 506 | if exc_type: 507 | return False 508 | return True 509 | 510 | -------------------------------------------------------------------------------- /pytoniq/liteclient/sync.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import time 4 | import typing 5 | 6 | from pytoniq_core.tl.block import BlockIdExt 7 | 8 | 9 | logger = logging.getLogger('sync') 10 | 11 | 12 | async def sync(client, to_block: BlockIdExt, init_block: BlockIdExt): 13 | logger.info(msg=f'syncing to {to_block}') 14 | from .client import LiteClient 15 | client: LiteClient 16 | valid_key_block_stored = False 17 | blocks_data = get_last_stored_blocks(init_block.root_hash.hex()) 18 | if not blocks_data: 19 | logger.debug(f'no last blocks were found, syncing from the init block {init_block}') 20 | mc_block = init_block 21 | key_block = init_block 22 | else: 23 | ttl, key_ts, key_block, mc_block = parse_blocks(blocks_data) 24 | logger.debug(f'found key block with ttl {ttl}') 25 | valid_key_block_stored = True 26 | if ttl <= time.time(): 27 | logger.debug(f'key block ttl has been expired, syncing from the init block {init_block}') 28 | mc_block = init_block 29 | key_block = init_block 30 | valid_key_block_stored = False 31 | 32 | """ 33 | Looks like last mc block should be be much sooner than last synced key block 34 | (cause we store key blocks with big ttl), but the lite node doesnt have to store any blocks except key blocks which 35 | persistent state has not expired. 36 | So the best solution is to ask liteserver if it remembers stored mc block and if not sync from the key block (or even init). 37 | """ 38 | try: 39 | best_key, best_key_ts = await client.get_mc_block_proof(known_block=mc_block, target_block=to_block, return_best_key_block=True) 40 | except: # TODO specify exception class 41 | best_key, best_key_ts = await client.get_mc_block_proof(known_block=key_block, target_block=to_block, return_best_key_block=True) 42 | 43 | if valid_key_block_stored: 44 | best_key, best_key_ts = choose_key_block(key_block, key_ts, best_key, best_key_ts) 45 | 46 | key_ttl = persistent_state_ttl(best_key_ts) 47 | logger.info(msg=f'synced! store key block {best_key} with ttl {key_ttl}') 48 | 49 | store_blocks(blocks_to_bytes(key_ttl, best_key_ts, best_key, to_block), True, init_block.root_hash.hex()) 50 | return True 51 | 52 | 53 | def get_block_store_path(): 54 | dir = os.path.join(os.path.curdir, '.blockstore') 55 | path = os.path.normpath(dir) 56 | if not os.path.isdir(path): 57 | os.mkdir(path) 58 | return path 59 | 60 | 61 | def get_last_stored_blocks(init_block_hash: str) -> typing.Optional[bytes]: 62 | path = get_block_store_path() 63 | files = [] 64 | for f in os.listdir(path): 65 | if init_block_hash in f: 66 | files.append(f) 67 | if not len(files): 68 | return None 69 | with open(os.path.join(path, files[-1]), 'rb') as f: 70 | result = f.read() 71 | return result 72 | 73 | 74 | def store_blocks(data: bytes, delete_old: bool = True, init_block_hash: str = None): 75 | path = get_block_store_path() 76 | if delete_old: 77 | for f in os.listdir(path): 78 | deleted = False 79 | if init_block_hash: 80 | if init_block_hash in f: 81 | os.remove(os.path.join(path, f)) 82 | deleted = True 83 | if not deleted: 84 | ttl = int(f[:8], 16) 85 | if ttl < time.time(): 86 | os.remove(os.path.join(path, f)) 87 | file_name = data[:88].hex() + init_block_hash + '.blks' 88 | with open(os.path.join(path, file_name), 'wb') as f: 89 | f.write(data) 90 | return 91 | 92 | 93 | def parse_blocks(data: bytes) -> typing.Tuple[int, int, BlockIdExt, BlockIdExt]: 94 | ttl = int.from_bytes(data[:4], 'big', signed=False) 95 | ts = int.from_bytes(data[4:8], 'big', signed=False) 96 | last_trusted_key_block = BlockIdExt.from_bytes(data[8:88]) 97 | last_trusted_mc_block = BlockIdExt.from_bytes(data[88:]) 98 | return ttl, ts, last_trusted_key_block, last_trusted_mc_block 99 | 100 | 101 | def blocks_to_bytes(ttl: int, ts: int, last_trusted_key_block: BlockIdExt, last_trusted_mc_block: BlockIdExt): 102 | return ttl.to_bytes(4, 'big', signed=False) + ts.to_bytes(4, 'big', signed=False) + last_trusted_key_block.to_bytes() + last_trusted_mc_block.to_bytes() 103 | 104 | 105 | def count_trailing_zeros(x: int): 106 | return (x & -x).bit_length() - 1 107 | 108 | 109 | def persistent_state_ttl(ts: int): 110 | # https://github.com/ton-blockchain/ton/blob/d2b418bb703ed6ccd89b7d40f9f1e44686012014/validator/interfaces/validator-manager.h#L176 111 | x = ts / (1 << 17) 112 | assert x > 0 113 | b = count_trailing_zeros(int(x)) 114 | return ts + ((1 << 18) << b) 115 | 116 | 117 | def choose_key_block(blk: BlockIdExt, blk_ts: int, other_blk: typing.Optional[BlockIdExt], other_ts: typing.Optional[int]): 118 | if other_blk is None: 119 | return blk, blk_ts 120 | if blk is None: 121 | return other_blk, other_ts 122 | p1 = persistent_state_ttl(blk_ts) 123 | p2 = persistent_state_ttl(other_ts) 124 | c_t = time.time() 125 | if p1 < c_t and p2 < c_t: 126 | if blk.seqno > other_blk.seqno: 127 | return blk, blk_ts 128 | else: 129 | return other_blk, other_ts 130 | if p1 < c_t: 131 | return other_blk, other_ts 132 | if p2 < c_t: 133 | return blk, blk_ts 134 | 135 | d1 = p1 - c_t 136 | d2 = p2 - c_t 137 | 138 | min_time = 21 * 3600 * 24 # 3 weeks 139 | if d1 >= min_time: 140 | if d2 < min_time: 141 | return blk, blk_ts 142 | else: 143 | if blk.seqno > other_blk.seqno: 144 | return blk, blk_ts 145 | else: 146 | return other_blk, other_ts 147 | elif d2 >= min_time: 148 | return other_blk, other_ts 149 | else: 150 | if blk.seqno > other_blk.seqno: 151 | return blk, blk_ts 152 | return other_blk, other_ts 153 | -------------------------------------------------------------------------------- /pytoniq/liteclient/utils.py: -------------------------------------------------------------------------------- 1 | from pytoniq_core.tl.block import BlockIdExt 2 | 3 | init_mainnet_blocks = [ 4 | BlockIdExt.from_dict({ 5 | "root_hash": "61192b72664cbcb06f8da9f0282c8bdf0e2871e18fb457e0c7cca6d502822bfe", 6 | "seqno": 27747086, 7 | "file_hash": "378db1ccf9c98c3944de1c4f5ce6fea4dcd7a26811b695f9019ccc3e7200e35b", 8 | "workchain": -1, 9 | "shard": -9223372036854775808 10 | }), 11 | BlockIdExt.from_dict({ 12 | "root_hash": "5695b27cd38b9bc46ab7a09967d7591aa2513b7372ae51c760dd64d682db27a8", 13 | "seqno": 34835953, 14 | "file_hash": "f28d76297e7806d24cf111110f527ded07b565693ad4b208c97c9d9419e2c4af", 15 | "workchain": -1, 16 | "shard": -9223372036854775808 17 | }), 18 | ] 19 | 20 | init_testnet_blocks = [ 21 | BlockIdExt.from_dict({ 22 | "file_hash": "c516b1814c204d76056f5e989d1f90f9556c7332e5ea3998c2fce143f9dcae1e", 23 | "seqno": 5176527, 24 | "root_hash": "4a83cba8c7bd0f3dba6093ce1833870294d27b98b491716d466461ff33cc1ae2", 25 | "workchain": -1, 26 | "shard": -9223372036854775808 27 | }) 28 | ] 29 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.31.0 2 | setuptools>=65.5.1 3 | pytoniq-core>=0.1.32 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="pytoniq", 8 | version="0.1.41", 9 | author="Maksim Kurbatov", 10 | author_email="cyrbatoff@gmail.com", 11 | description="TON Blockchain SDK", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | packages=setuptools.find_packages('.', exclude=['tests', 'examples']), 15 | classifiers=[ 16 | "Programming Language :: Python :: 3.9", 17 | "Programming Language :: Python :: 3.10", 18 | "Programming Language :: Python :: 3.11", 19 | "Topic :: Software Development :: Libraries", 20 | ], 21 | url="https://github.com/yungwine/pytoniq", 22 | python_requires='>=3.9', 23 | py_modules=["pytoniq"], 24 | install_requires=[ 25 | "pytoniq-core>=0.1.42", 26 | "requests>=2.31.0", 27 | "setuptools>=65.5.1", 28 | ], 29 | extras_require={ 30 | 'tvm': ['pytvm>=0.0.11'], 31 | } 32 | ) 33 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yungwine/pytoniq/04e962de7fec6ffb93e091c2394d6ac462a13b78/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_adnl.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | 4 | from pytoniq.adnl.adnl import AdnlTransport, Node 5 | 6 | 7 | adnl = AdnlTransport(timeout=3) 8 | 9 | 10 | @pytest.mark.asyncio 11 | async def test_connection(): 12 | 13 | # start adnl receiving server 14 | await adnl.start() 15 | 16 | # take peer from public config 17 | peer = Node('172.104.59.125', 14432, "/YDNd+IwRUgL0mq21oC0L3RxrS8gTu0nciSPUrhqR78=", adnl) 18 | await adnl.connect_to_peer(peer) 19 | 20 | # ask peer for something 21 | await peer.get_signed_address_list() 22 | 23 | # send pings to peer 24 | await asyncio.sleep(2) 25 | 26 | await peer.disconnect() 27 | 28 | # add another peer 29 | peer = Node('5.161.60.160', 12485, "jXiLaOQz1HPayilWgBWhV9xJhUIqfU95t+KFKQPIpXg=", adnl) 30 | 31 | # second way to connect to peer 32 | await peer.connect() 33 | 34 | # 2 adnl channels 35 | print(adnl.channels) 36 | 37 | # check for pings 38 | await asyncio.sleep(10) 39 | 40 | # stop adnl receiving server 41 | await adnl.close() 42 | -------------------------------------------------------------------------------- /tests/test_balancer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import pytest_asyncio 3 | 4 | from pytoniq import LiteBalancer 5 | 6 | from pytoniq_core.tlb.config import ConfigParam0, ConfigParam1 7 | from pytoniq_core import Address, Cell 8 | 9 | 10 | @pytest.mark.asyncio 11 | async def test_init(): 12 | client = LiteBalancer.from_mainnet_config(trust_level=1) 13 | await client.start_up() 14 | await client.close_all() 15 | 16 | client = LiteBalancer.from_testnet_config(trust_level=1) 17 | await client.start_up() 18 | await client.close_all() 19 | 20 | 21 | @pytest_asyncio.fixture 22 | async def client(): 23 | client = LiteBalancer.from_mainnet_config(trust_level=1) 24 | await client.start_up() 25 | return client 26 | 27 | 28 | @pytest.mark.asyncio 29 | async def test_account_state(client: LiteBalancer): 30 | params = await client.get_config_params(params=[0]) 31 | param: ConfigParam0 = params[0] 32 | state, _ = await client.raw_get_account_state(Address((-1, param.config_addr))) 33 | data: Cell = state.storage.state.state_init.data 34 | cs = data.begin_parse() 35 | cs.skip_bits(32) # seqno 36 | assert cs.load_bytes(32) == b'\x82\xb1|\xaa\xdb0=S\xc3(l\x06\xa6\xe1\xaf\xfcQ}\x1b\xc1\xd3\xef.D\x89\xd1\x8b\x87?]|\xd1' 37 | await client.close_all() 38 | 39 | 40 | @pytest.mark.asyncio 41 | async def test_archival(client: LiteBalancer): 42 | blk, _ = await client.lookup_block(-1, -2 ** 63, 10, only_archive=True) 43 | assert blk.root_hash.hex() == 'c1b8e9cb4c3d886d91764d243693119f4972d284ce7be01e739b67fdcbb84ca1' 44 | 45 | 46 | @pytest.mark.asyncio 47 | async def test_transactions(client: LiteBalancer): 48 | params = await client.get_config_params(params=[1]) 49 | param: ConfigParam1 = params[1] 50 | trs = await client.get_transactions(Address((-1, param.elector_addr)), count=200) 51 | assert len(trs) == 200 52 | await client.close_all() 53 | -------------------------------------------------------------------------------- /tests/test_dht.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pytest 3 | 4 | from pytoniq.adnl.adnl import AdnlTransport 5 | from pytoniq.adnl.dht import DhtClient, DhtNode 6 | 7 | 8 | adnl = AdnlTransport(timeout=5) 9 | 10 | 11 | client = DhtClient.from_mainnet_config(adnl) 12 | 13 | 14 | @pytest.mark.asyncio 15 | async def test_dht(): 16 | logging.basicConfig(level=logging.DEBUG) 17 | await adnl.start() 18 | 19 | foundation_adnl_addr = '516618cf6cbe9004f6883e742c9a2e3ca53ed02e3e36f4cef62a98ee1e449174' 20 | resp = await client.find_value(key=DhtClient.get_dht_key_id(bytes.fromhex(foundation_adnl_addr))) 21 | print(resp) 22 | 23 | assert resp['@type'] == 'dht.valueFound' 24 | -------------------------------------------------------------------------------- /tests/test_liteclient.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | 4 | import pytest 5 | import random 6 | 7 | import pytest_asyncio 8 | 9 | from pytoniq import LiteClient 10 | 11 | 12 | @pytest_asyncio.fixture 13 | async def client(): 14 | while True: 15 | client = LiteClient.from_mainnet_config(random.randint(0, 15), trust_level=1) 16 | try: 17 | await client.connect() 18 | yield client 19 | await client.close() 20 | return 21 | except: 22 | continue 23 | 24 | 25 | @pytest.mark.asyncio 26 | async def test_init(): 27 | client = LiteClient.from_mainnet_config(ls_i=0, trust_level=2) 28 | await client.connect() 29 | await client.reconnect() 30 | await client.close() 31 | 32 | client = LiteClient.from_testnet_config(ls_i=12, trust_level=2) 33 | await client.connect() 34 | await client.reconnect() 35 | await client.close() 36 | 37 | # try: 38 | # client = LiteClient.from_mainnet_config(random.randint(0, 8), trust_level=0) 39 | # await client.connect() 40 | # await client.close() 41 | # except asyncio.TimeoutError: 42 | # print('skipping') 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_methods(client: LiteClient): 47 | await client.get_masterchain_info() 48 | await client.get_config_all() 49 | await client.raw_get_block(client.last_mc_block) 50 | lib = 'c245262b8c2bce5e9fcd23ca334e1d55fa96d4ce69aa2817ded717cefcba3f73' 51 | fake_lib = '0000000000000000000000000000000000000000000000000000000000000000' 52 | res = await client.get_libraries([lib, lib, fake_lib, lib]) 53 | assert len(res) == 2 54 | assert res[lib].hash.hex() == lib 55 | assert res[fake_lib] is None 56 | 57 | 58 | @pytest.mark.asyncio 59 | async def test_get_method(client: LiteClient): 60 | 61 | result = await client.run_get_method(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', method='seqno', 62 | stack=[]) 63 | assert isinstance(result[0], int) 64 | result2 = await client.run_get_method_local(address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', method='seqno', 65 | stack=[]) 66 | assert result2 == result 67 | -------------------------------------------------------------------------------- /tests/test_wallet.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | import random 5 | 6 | import pytest_asyncio 7 | 8 | from pytoniq import BaseWallet, WalletV3R1, WalletV3R2, WalletV4R2 9 | 10 | 11 | @pytest_asyncio.fixture 12 | async def client(): 13 | c = Mock() 14 | 15 | async def raw_get_account_state(*args, **kwargs): 16 | return None, None 17 | 18 | c.raw_get_account_state = raw_get_account_state 19 | return c 20 | 21 | 22 | @pytest.mark.asyncio 23 | async def test_wallets(client): 24 | m0, w0 = await BaseWallet.create(client, version='v3r1') 25 | m1, w1 = await WalletV3R2.create(client) 26 | m2, w2 = await WalletV4R2.create(client) 27 | w3 = await WalletV3R2.from_mnemonic(client, mnemonics=m1) 28 | w4 = await WalletV4R2.from_mnemonic(client, mnemonics=m2) 29 | w5 = await BaseWallet.from_mnemonic(client, mnemonics=m0, version='v3r1') 30 | w6 = await WalletV3R1.from_mnemonic(client, mnemonics=m0) 31 | 32 | assert w0.address == w5.address == w6.address 33 | assert w1.address == w3.address 34 | assert w2.address == w4.address 35 | --------------------------------------------------------------------------------