├── .gitignore ├── LICENSE ├── README.md ├── contracts ├── auction-get-met.func ├── highload-wallet-v2-code.fc ├── jetton-minter.fc ├── jetton-wallet.fc ├── multisig-code-2.fc ├── nft-collection.fc ├── nft-fixprice-sale-v3.fc └── nft-item.fc ├── lesson_1 ├── README.md ├── code.func ├── counter.py ├── mnemoincs.py ├── tontools.py ├── wallet │ ├── .DS_Store │ ├── .gitignore │ ├── README.md │ ├── fift │ │ ├── .gitkeep │ │ ├── data.fif │ │ ├── examples │ │ │ └── parse_get_cell.fif │ │ └── usage.fif │ ├── func │ │ └── code.func │ ├── project.yaml │ └── tests │ │ └── .gitkeep └── wallets.py ├── lesson_10 ├── README.md ├── client.py ├── config.json └── tontools.py ├── lesson_2 ├── README.md ├── blocks.py ├── cells.py ├── client.py └── liteservers.py ├── lesson_3 ├── README.md ├── fail_transaction.py ├── parse_tr_status.py ├── parse_tr_type.py └── wallets.py ├── lesson_4 ├── README.md ├── client.py ├── mint.py ├── mint_bodies.py ├── mnemoincs.py ├── transfers.py └── wallets.py ├── lesson_5 ├── README.md ├── client.py ├── get_items_by_collection.py ├── parse_collection.py ├── parse_nft_items.py └── parse_sale.py ├── lesson_6 ├── README.md ├── burn.py ├── client.py ├── deploy.py ├── encode_image.py ├── image.raw ├── image.webp ├── metadata.json ├── mint_bodies.py ├── mnemoincs.py ├── transfer.py └── wallets.py ├── lesson_7 ├── README.md ├── client.py ├── jetton_minter_parsing.py └── jetton_wallet_parsing.py ├── lesson_8 ├── README.md ├── client.py ├── config.py ├── deploy.py ├── get_processed.py ├── mnemoincs.py ├── parse_hashmap.py ├── transfer.py └── wallets.py └── lesson_9 ├── README.md ├── client.py ├── get_multisig.py ├── offchain ├── client.py ├── general.py ├── get_multisig.py ├── main.py └── owner_1.py └── onchain ├── client.py ├── general.py ├── get_multisig.py ├── owner_0.py └── owner_1.py /.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 | /lesson_1/secret.py 162 | /lesson_3/secret.py 163 | /tests/ 164 | 165 | /lesson_6/secret.py 166 | /lesson_7/secret.py 167 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PYTHON TON LESSONS 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | ![](https://img.shields.io/github/last-commit/TonDevStudy/pyton-lessons-eng) 4 | 5 | Russian version of these lessons can be found [here](https://github.com/yungwine/pyton-lessons) 6 | 7 | ### Useful repos/libs: 8 | * [tonsdk](https://github.com/tonfactory/tonsdk) 9 | * [toncenter's pytonlib](https://github.com/toncenter/pytonlib) 10 | * [TonTools](https://github.com/yungwine/TonTools) 11 | * [ton-http-api](https://github.com/toncenter/ton-http-api) 12 | * [mytonctrl](https://github.com/ton-blockchain/mytonctrl) 13 | 14 | ### Official Docs - [link](https://docs.ton.org) 15 | 16 | ## [LESSON 1](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_1) - [YouTube](https://www.youtube.com/watch?v=LJqam4eBqyE) 17 | 18 | - General libraries review 19 | - Wallet creation, import and deployment 20 | - Contract deployment via internal message 21 | - Counter contract deployment 22 | - Internal message creation 23 | - Contract get method parsing 24 | 25 | ## [LESSON 2](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_2) - [YouTube](https://www.youtube.com/watch?v=ipFDBjJFLCw) 26 | 27 | - Cell creation 28 | - Slice parsing 29 | - Work with liteservers 30 | - Looking for archive ls from public ls list 31 | - Transactions parsing from blocks 32 | - Listening for new transactions in the blockchain 33 | 34 | ## [LESSON 3](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_3) - [YouTube](https://www.youtube.com/watch?v=f3u0g84dFhY) 35 | 36 | - Transactions data parsing 37 | - Transactions statuses 38 | - Messages parsing 39 | - Transactions types 40 | - Jetton transfer parsing 41 | - Jetton burning parsing 42 | 43 | ## [LESSON 4](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_4) - [YouTube](https://www.youtube.com/watch?v=mBDSZnqpDbo) 44 | 45 | - NFT collection creation and deployment 46 | - NFT (batch) minting 47 | - NFT transfer 48 | 49 | ## [LESSON 5](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_5) - [YouTube](https://www.youtube.com/watch?v=XPL97vlmfts) 50 | 51 | - NFT collection parsing via get methods 52 | - Single NFT items parsing 53 | - NFT sale (auction) contracts parsing 54 | - Getting all NFT items by collection address 55 | 56 | ## [LESSON 6](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_6) - [YouTube](https://www.youtube.com/watch?v=cT9M54Y3uc8) 57 | 58 | - Jetton minter deployment 59 | - Jettons mint 60 | - Jettons transfer 61 | - Jettons burn 62 | 63 | ## [LESSON 7](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_7) - [YouTube](https://www.youtube.com/watch?v=7WhF15NA5P8) 64 | 65 | - Jetton content parsing 66 | - Jetton onchain and offchain metadata parsing 67 | - HashMaps introduction 68 | - Jetton wallet parsing 69 | 70 | ## [LESSON 8](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_8) - [YouTube](https://youtu.be/GLGGk_akkJk) 71 | 72 | - Highload wallets: deployment, transfer 73 | - HashMaps reading 74 | - Blockchain config parsing 75 | 76 | ## [LESSON 9](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_9) - [YouTube](https://www.youtube.com/watch?v=1pM3mPYZGtQ) 77 | 78 | - Multisig wallets creation 79 | - Off-chain and on-chain signatures 80 | - Reading contracts data 81 | 82 | ## [LESSON 10](https://github.com/TonDevStudy/pyton-lessons-eng/blob/main/lesson_10) - [YouTube](https://www.youtube.com/watch?v=x4rG-aFOqNo) 83 | 84 | - TON Node types 85 | - Launching Liteserver using mytonctrl 86 | - Launching local toncenter 87 | - interaction with your own Liteserver -------------------------------------------------------------------------------- /contracts/auction-get-met.func: -------------------------------------------------------------------------------- 1 | ;; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 2 | (int, int, int, slice, slice, slice, int, slice, int, slice, int, int, slice, int, int, int, int, int, int, int) get_sale_data() method_id { 3 | init_data(); 4 | 5 | var ( 6 | mp_fee_addr, 7 | mp_fee_factor, 8 | mp_fee_base, 9 | royalty_fee_addr, 10 | royalty_fee_factor, 11 | royalty_fee_base 12 | ) = get_fees(); 13 | 14 | return ( 15 | 0x415543, ;; 1 nft aucion ("AUC") 16 | end?, ;; 2 17 | end_time, ;; 3 18 | mp_addr, ;; 4 19 | nft_addr, ;; 5 20 | nft_owner, ;; 6 21 | last_bid, ;; 7 22 | last_member, ;; 8 23 | min_step, ;; 9 24 | mp_fee_addr, ;; 10 25 | mp_fee_factor, mp_fee_base, ;; 11, 12 26 | royalty_fee_addr, ;; 13 27 | royalty_fee_factor, royalty_fee_base, ;; 14, 15 28 | max_bid, ;; 16 29 | min_bid, ;; 17 30 | created_at?, ;; 18 31 | last_bid_at, ;; 19 32 | is_canceled? ;; 20 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /contracts/highload-wallet-v2-code.fc: -------------------------------------------------------------------------------- 1 | ;; Heavy-duty wallet for mass transfers (e.g., for cryptocurrency exchanges) 2 | ;; accepts orders for up to 254 internal messages (transfers) in one external message 3 | ;; this version does not use seqno for replay protection; instead, it remembers all recent query_ids 4 | ;; in this way several external messages with different query_id can be sent in parallel 5 | 6 | () recv_internal(slice in_msg) impure { 7 | ;; do nothing for internal messages 8 | } 9 | 10 | () recv_external(slice in_msg) impure { 11 | var signature = in_msg~load_bits(512); 12 | var cs = in_msg; 13 | var (subwallet_id, query_id) = (cs~load_uint(32), cs~load_uint(64)); 14 | var bound = (now() << 32); 15 | throw_if(35, query_id < bound); 16 | var ds = get_data().begin_parse(); 17 | var (stored_subwallet, last_cleaned, public_key, old_queries) = (ds~load_uint(32), ds~load_uint(64), ds~load_uint(256), ds~load_dict()); 18 | ds.end_parse(); 19 | (_, var found?) = old_queries.udict_get?(64, query_id); 20 | throw_if(32, found?); 21 | throw_unless(34, subwallet_id == stored_subwallet); 22 | throw_unless(35, check_signature(slice_hash(in_msg), signature, public_key)); 23 | var dict = cs~load_dict(); 24 | cs.end_parse(); 25 | accept_message(); 26 | int i = -1; 27 | do { 28 | (i, var cs, var f) = dict.idict_get_next?(16, i); 29 | if (f) { 30 | var mode = cs~load_uint(8); 31 | send_raw_message(cs~load_ref(), mode); 32 | } 33 | } until (~ f); 34 | bound -= (64 << 32); ;; clean up records expired more than 64 seconds ago 35 | old_queries~udict_set_builder(64, query_id, begin_cell()); 36 | var queries = old_queries; 37 | do { 38 | var (old_queries', i, _, f) = old_queries.udict_delete_get_min(64); 39 | f~touch(); 40 | if (f) { 41 | f = (i < bound); 42 | } 43 | if (f) { 44 | old_queries = old_queries'; 45 | last_cleaned = i; 46 | } 47 | } until (~ f); 48 | set_data(begin_cell() 49 | .store_uint(stored_subwallet, 32) 50 | .store_uint(last_cleaned, 64) 51 | .store_uint(public_key, 256) 52 | .store_dict(old_queries) 53 | .end_cell()); 54 | } 55 | 56 | ;; Get methods 57 | 58 | ;; returns -1 for processed queries, 0 for unprocessed, 1 for unknown (forgotten) 59 | int processed?(int query_id) method_id { 60 | var ds = get_data().begin_parse(); 61 | var (_, last_cleaned, _, old_queries) = (ds~load_uint(32), ds~load_uint(64), ds~load_uint(256), ds~load_dict()); 62 | ds.end_parse(); 63 | (_, var found) = old_queries.udict_get?(64, query_id); 64 | return found ? true : - (query_id <= last_cleaned); 65 | } 66 | 67 | int get_public_key() method_id { 68 | var cs = get_data().begin_parse(); 69 | cs~load_uint(32 + 64); 70 | return cs.preload_uint(256); 71 | } 72 | -------------------------------------------------------------------------------- /contracts/jetton-minter.fc: -------------------------------------------------------------------------------- 1 | ;; Jettons discoverable smart contract 2 | 3 | ;; storage scheme 4 | ;; storage#_ total_supply:Coins admin_address:MsgAddress content:^Cell jetton_wallet_code:^Cell = Storage; 5 | 6 | #include "imports/stdlib.fc"; 7 | #include "imports/params.fc"; 8 | #include "imports/constants.fc"; 9 | #include "imports/jetton-utils.fc"; 10 | #include "imports/op-codes.fc"; 11 | #include "imports/utils.fc"; 12 | #include "imports/discovery-params.fc"; 13 | #pragma version >=0.2.0; 14 | 15 | (int, slice, cell, cell) load_data() inline { 16 | slice ds = get_data().begin_parse(); 17 | return ( 18 | ds~load_coins(), ;; total_supply 19 | ds~load_msg_addr(), ;; admin_address 20 | ds~load_ref(), ;; content 21 | ds~load_ref() ;; jetton_wallet_code 22 | ); 23 | } 24 | 25 | () save_data(int total_supply, slice admin_address, cell content, cell jetton_wallet_code) impure inline { 26 | set_data(begin_cell() 27 | .store_coins(total_supply) 28 | .store_slice(admin_address) 29 | .store_ref(content) 30 | .store_ref(jetton_wallet_code) 31 | .end_cell() 32 | ); 33 | } 34 | 35 | () mint_tokens(slice to_address, cell jetton_wallet_code, int amount, cell master_msg) impure { 36 | cell state_init = calculate_jetton_wallet_state_init(to_address, my_address(), jetton_wallet_code); 37 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 38 | var msg = begin_cell() 39 | .store_uint(0x18, 6) 40 | .store_slice(to_wallet_address) 41 | .store_coins(amount) 42 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 43 | .store_ref(state_init) 44 | .store_ref(master_msg); 45 | send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors 46 | } 47 | 48 | () recv_internal(int msg_value, cell in_msg_full, slice in_msg_body) impure { 49 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 50 | return (); 51 | } 52 | slice cs = in_msg_full.begin_parse(); 53 | int flags = cs~load_uint(4); 54 | 55 | if (flags & 1) { ;; ignore all bounced messages 56 | return (); 57 | } 58 | slice sender_address = cs~load_msg_addr(); 59 | cs~load_msg_addr(); ;; skip dst 60 | cs~load_coins(); ;; skip value 61 | cs~skip_bits(1); ;; skip extracurrency collection 62 | cs~load_coins(); ;; skip ihr_fee 63 | int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of provide_wallet_address cost 64 | 65 | int op = in_msg_body~load_uint(32); 66 | int query_id = in_msg_body~load_uint(64); 67 | 68 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 69 | 70 | if (op == op::mint()) { 71 | throw_unless(73, equal_slices(sender_address, admin_address)); 72 | slice to_address = in_msg_body~load_msg_addr(); 73 | int amount = in_msg_body~load_coins(); 74 | cell master_msg = in_msg_body~load_ref(); 75 | slice master_msg_cs = master_msg.begin_parse(); 76 | master_msg_cs~skip_bits(32 + 64); ;; op + query_id 77 | int jetton_amount = master_msg_cs~load_coins(); 78 | mint_tokens(to_address, jetton_wallet_code, amount, master_msg); 79 | save_data(total_supply + jetton_amount, admin_address, content, jetton_wallet_code); 80 | return (); 81 | } 82 | 83 | if (op == op::burn_notification()) { 84 | int jetton_amount = in_msg_body~load_coins(); 85 | slice from_address = in_msg_body~load_msg_addr(); 86 | throw_unless(74, 87 | equal_slices(calculate_user_jetton_wallet_address(from_address, my_address(), jetton_wallet_code), sender_address) 88 | ); 89 | save_data(total_supply - jetton_amount, admin_address, content, jetton_wallet_code); 90 | slice response_address = in_msg_body~load_msg_addr(); 91 | if (response_address.preload_uint(2) != 0) { 92 | var msg = begin_cell() 93 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000 94 | .store_slice(response_address) 95 | .store_coins(0) 96 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 97 | .store_uint(op::excesses(), 32) 98 | .store_uint(query_id, 64); 99 | send_raw_message(msg.end_cell(), 2 + 64); 100 | } 101 | return (); 102 | } 103 | 104 | if (op == op::provide_wallet_address()) { 105 | throw_unless(75, msg_value > fwd_fee + const::provide_address_gas_consumption()); 106 | 107 | slice owner_address = in_msg_body~load_msg_addr(); 108 | int include_address? = in_msg_body~load_uint(1); 109 | 110 | cell included_address = include_address? 111 | ? begin_cell().store_slice(owner_address).end_cell() 112 | : null(); 113 | 114 | var msg = begin_cell() 115 | .store_uint(0x18, 6) 116 | .store_slice(sender_address) 117 | .store_coins(0) 118 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 119 | .store_uint(op::take_wallet_address(), 32) 120 | .store_uint(query_id, 64); 121 | 122 | if (is_resolvable?(owner_address)) { 123 | msg = msg.store_slice(calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code)); 124 | } else { 125 | msg = msg.store_uint(0, 2); ;; addr_none 126 | } 127 | send_raw_message(msg.store_maybe_ref(included_address).end_cell(), 64); 128 | return (); 129 | } 130 | 131 | if (op == 3) { ;; change admin 132 | throw_unless(73, equal_slices(sender_address, admin_address)); 133 | slice new_admin_address = in_msg_body~load_msg_addr(); 134 | save_data(total_supply, new_admin_address, content, jetton_wallet_code); 135 | return (); 136 | } 137 | 138 | if (op == 4) { ;; change content, delete this for immutable tokens 139 | throw_unless(73, equal_slices(sender_address, admin_address)); 140 | save_data(total_supply, admin_address, in_msg_body~load_ref(), jetton_wallet_code); 141 | return (); 142 | } 143 | 144 | throw(0xffff); 145 | } 146 | 147 | (int, int, slice, cell, cell) get_jetton_data() method_id { 148 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 149 | return (total_supply, -1, admin_address, content, jetton_wallet_code); 150 | } 151 | 152 | slice get_wallet_address(slice owner_address) method_id { 153 | (int total_supply, slice admin_address, cell content, cell jetton_wallet_code) = load_data(); 154 | return calculate_user_jetton_wallet_address(owner_address, my_address(), jetton_wallet_code); 155 | } -------------------------------------------------------------------------------- /contracts/jetton-wallet.fc: -------------------------------------------------------------------------------- 1 | ;; Jetton Wallet Smart Contract 2 | 3 | #include "imports/stdlib.fc"; 4 | #include "imports/params.fc"; 5 | #include "imports/constants.fc"; 6 | #include "imports/jetton-utils.fc"; 7 | #include "imports/op-codes.fc"; 8 | #include "imports/utils.fc"; 9 | #pragma version >=0.2.0; 10 | 11 | {- 12 | 13 | NOTE that this tokens can be transferred within the same workchain. 14 | 15 | This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions: 16 | 17 | 1) use more expensive but universal function to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`) 18 | 19 | 2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain) 20 | 21 | -} 22 | 23 | const min_tons_for_storage = 10000000; ;; 0.01 TON 24 | const gas_consumption = 10000000; ;; 0.01 TON 25 | 26 | {- 27 | Storage 28 | storage#_ balance:Coins owner_address:MsgAddressInt jetton_master_address:MsgAddressInt jetton_wallet_code:^Cell = Storage; 29 | -} 30 | 31 | (int, slice, slice, cell) load_data() inline { 32 | slice ds = get_data().begin_parse(); 33 | return (ds~load_coins(), ds~load_msg_addr(), ds~load_msg_addr(), ds~load_ref()); 34 | } 35 | 36 | () save_data (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) impure inline { 37 | set_data(pack_jetton_wallet_data(balance, owner_address, jetton_master_address, jetton_wallet_code)); 38 | } 39 | 40 | {- 41 | transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 42 | response_destination:MsgAddress custom_payload:(Maybe ^Cell) 43 | forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 44 | = InternalMsgBody; 45 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 46 | response_address:MsgAddress 47 | forward_ton_amount:(VarUInteger 16) 48 | forward_payload:(Either Cell ^Cell) 49 | = InternalMsgBody; 50 | -} 51 | 52 | () send_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { 53 | int query_id = in_msg_body~load_uint(64); 54 | int jetton_amount = in_msg_body~load_coins(); 55 | slice to_owner_address = in_msg_body~load_msg_addr(); 56 | force_chain(to_owner_address); 57 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 58 | balance -= jetton_amount; 59 | 60 | throw_unless(705, equal_slices(owner_address, sender_address)); 61 | throw_unless(706, balance >= 0); 62 | 63 | cell state_init = calculate_jetton_wallet_state_init(to_owner_address, jetton_master_address, jetton_wallet_code); 64 | slice to_wallet_address = calculate_jetton_wallet_address(state_init); 65 | slice response_address = in_msg_body~load_msg_addr(); 66 | cell custom_payload = in_msg_body~load_dict(); 67 | int forward_ton_amount = in_msg_body~load_coins(); 68 | throw_unless(708, slice_bits(in_msg_body) >= 1); 69 | slice either_forward_payload = in_msg_body; 70 | var msg = begin_cell() 71 | .store_uint(0x18, 6) 72 | .store_slice(to_wallet_address) 73 | .store_coins(0) 74 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 75 | .store_ref(state_init); 76 | var msg_body = begin_cell() 77 | .store_uint(op::internal_transfer(), 32) 78 | .store_uint(query_id, 64) 79 | .store_coins(jetton_amount) 80 | .store_slice(owner_address) 81 | .store_slice(response_address) 82 | .store_coins(forward_ton_amount) 83 | .store_slice(either_forward_payload) 84 | .end_cell(); 85 | 86 | msg = msg.store_ref(msg_body); 87 | int fwd_count = forward_ton_amount ? 2 : 1; 88 | throw_unless(709, msg_value > 89 | forward_ton_amount + 90 | ;; 3 messages: wal1->wal2, wal2->owner, wal2->response 91 | ;; but last one is optional (it is ok if it fails) 92 | fwd_count * fwd_fee + 93 | (2 * gas_consumption + min_tons_for_storage)); 94 | ;; universal message send fee calculation may be activated here 95 | ;; by using this instead of fwd_fee 96 | ;; msg_fwd_fee(to_wallet, msg_body, state_init, 15) 97 | 98 | send_raw_message(msg.end_cell(), 64); ;; revert on errors 99 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 100 | } 101 | 102 | {- 103 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 104 | response_address:MsgAddress 105 | forward_ton_amount:(VarUInteger 16) 106 | forward_payload:(Either Cell ^Cell) 107 | = InternalMsgBody; 108 | -} 109 | 110 | () receive_tokens (slice in_msg_body, slice sender_address, int my_ton_balance, int fwd_fee, int msg_value) impure { 111 | ;; NOTE we can not allow fails in action phase since in that case there will be 112 | ;; no bounce. Thus check and throw in computation phase. 113 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 114 | int query_id = in_msg_body~load_uint(64); 115 | int jetton_amount = in_msg_body~load_coins(); 116 | balance += jetton_amount; 117 | slice from_address = in_msg_body~load_msg_addr(); 118 | slice response_address = in_msg_body~load_msg_addr(); 119 | throw_unless(707, 120 | equal_slices(jetton_master_address, sender_address) 121 | | 122 | equal_slices(calculate_user_jetton_wallet_address(from_address, jetton_master_address, jetton_wallet_code), sender_address) 123 | ); 124 | int forward_ton_amount = in_msg_body~load_coins(); 125 | 126 | int ton_balance_before_msg = my_ton_balance - msg_value; 127 | int storage_fee = min_tons_for_storage - min(ton_balance_before_msg, min_tons_for_storage); 128 | msg_value -= (storage_fee + gas_consumption); 129 | if(forward_ton_amount) { 130 | msg_value -= (forward_ton_amount + fwd_fee); 131 | slice either_forward_payload = in_msg_body; 132 | 133 | var msg_body = begin_cell() 134 | .store_uint(op::transfer_notification(), 32) 135 | .store_uint(query_id, 64) 136 | .store_coins(jetton_amount) 137 | .store_slice(from_address) 138 | .store_slice(either_forward_payload) 139 | .end_cell(); 140 | 141 | var msg = begin_cell() 142 | .store_uint(0x10, 6) ;; we should not bounce here cause receiver can have uninitialized contract 143 | .store_slice(owner_address) 144 | .store_coins(forward_ton_amount) 145 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 146 | .store_ref(msg_body); 147 | 148 | send_raw_message(msg.end_cell(), 1); 149 | } 150 | 151 | if ((response_address.preload_uint(2) != 0) & (msg_value > 0)) { 152 | var msg = begin_cell() 153 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 010000 154 | .store_slice(response_address) 155 | .store_coins(msg_value) 156 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 157 | .store_uint(op::excesses(), 32) 158 | .store_uint(query_id, 64); 159 | send_raw_message(msg.end_cell(), 2); 160 | } 161 | 162 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 163 | } 164 | 165 | () burn_tokens (slice in_msg_body, slice sender_address, int msg_value, int fwd_fee) impure { 166 | ;; NOTE we can not allow fails in action phase since in that case there will be 167 | ;; no bounce. Thus check and throw in computation phase. 168 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 169 | int query_id = in_msg_body~load_uint(64); 170 | int jetton_amount = in_msg_body~load_coins(); 171 | slice response_address = in_msg_body~load_msg_addr(); 172 | ;; ignore custom payload 173 | ;; slice custom_payload = in_msg_body~load_dict(); 174 | balance -= jetton_amount; 175 | throw_unless(705, equal_slices(owner_address, sender_address)); 176 | throw_unless(706, balance >= 0); 177 | throw_unless(707, msg_value > fwd_fee + 2 * gas_consumption); 178 | 179 | var msg_body = begin_cell() 180 | .store_uint(op::burn_notification(), 32) 181 | .store_uint(query_id, 64) 182 | .store_coins(jetton_amount) 183 | .store_slice(owner_address) 184 | .store_slice(response_address) 185 | .end_cell(); 186 | 187 | var msg = begin_cell() 188 | .store_uint(0x18, 6) 189 | .store_slice(jetton_master_address) 190 | .store_coins(0) 191 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 192 | .store_ref(msg_body); 193 | 194 | send_raw_message(msg.end_cell(), 64); 195 | 196 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 197 | } 198 | 199 | () on_bounce (slice in_msg_body) impure { 200 | in_msg_body~skip_bits(32); ;; 0xFFFFFFFF 201 | (int balance, slice owner_address, slice jetton_master_address, cell jetton_wallet_code) = load_data(); 202 | int op = in_msg_body~load_uint(32); 203 | throw_unless(709, (op == op::internal_transfer()) | (op == op::burn_notification())); 204 | int query_id = in_msg_body~load_uint(64); 205 | int jetton_amount = in_msg_body~load_coins(); 206 | balance += jetton_amount; 207 | save_data(balance, owner_address, jetton_master_address, jetton_wallet_code); 208 | } 209 | 210 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 211 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 212 | return (); 213 | } 214 | 215 | slice cs = in_msg_full.begin_parse(); 216 | int flags = cs~load_uint(4); 217 | if (flags & 1) { 218 | on_bounce(in_msg_body); 219 | return (); 220 | } 221 | slice sender_address = cs~load_msg_addr(); 222 | cs~load_msg_addr(); ;; skip dst 223 | cs~load_coins(); ;; skip value 224 | cs~skip_bits(1); ;; skip extracurrency collection 225 | cs~load_coins(); ;; skip ihr_fee 226 | int fwd_fee = muldiv(cs~load_coins(), 3, 2); ;; we use message fwd_fee for estimation of forward_payload costs 227 | 228 | int op = in_msg_body~load_uint(32); 229 | 230 | if (op == op::transfer()) { ;; outgoing transfer 231 | send_tokens(in_msg_body, sender_address, msg_value, fwd_fee); 232 | return (); 233 | } 234 | 235 | if (op == op::internal_transfer()) { ;; incoming transfer 236 | receive_tokens(in_msg_body, sender_address, my_balance, fwd_fee, msg_value); 237 | return (); 238 | } 239 | 240 | if (op == op::burn()) { ;; burn 241 | burn_tokens(in_msg_body, sender_address, msg_value, fwd_fee); 242 | return (); 243 | } 244 | 245 | throw(0xffff); 246 | } 247 | 248 | (int, slice, slice, cell) get_wallet_data() method_id { 249 | return load_data(); 250 | } 251 | -------------------------------------------------------------------------------- /contracts/multisig-code-2.fc: -------------------------------------------------------------------------------- 1 | ;; Simple wallet smart contract 2 | 3 | _ unpack_state() inline_ref { 4 | var ds = begin_parse(get_data()); 5 | var res = (ds~load_uint(32), ds~load_uint(8), ds~load_uint(8), ds~load_uint(64), ds~load_dict(), ds~load_dict()); 6 | ds.end_parse(); 7 | return res; 8 | } 9 | 10 | _ pack_state(cell pending_queries, cell owner_infos, int last_cleaned, int k, int n, int wallet_id) inline_ref { 11 | return begin_cell() 12 | .store_uint(wallet_id, 32) 13 | .store_uint(n, 8) 14 | .store_uint(k, 8) 15 | .store_uint(last_cleaned, 64) 16 | .store_dict(owner_infos) 17 | .store_dict(pending_queries) 18 | .end_cell(); 19 | } 20 | 21 | _ pack_owner_info(int public_key, int flood) inline_ref { 22 | return begin_cell() 23 | .store_uint(public_key, 256) 24 | .store_uint(flood, 8); 25 | } 26 | 27 | _ unpack_owner_info(slice cs) inline_ref { 28 | return (cs~load_uint(256), cs~load_uint(8)); 29 | } 30 | 31 | (int, int) check_signatures(cell public_keys, cell signatures, int hash, int cnt_bits) impure inline_ref { 32 | int cnt = 0; 33 | 34 | do { 35 | slice cs = signatures.begin_parse(); 36 | slice signature = cs~load_bits(512); 37 | 38 | int i = cs~load_uint(8); 39 | signatures = cs~load_dict(); 40 | 41 | (slice public_key, var found?) = public_keys.udict_get?(8, i); 42 | throw_unless(37, found?); 43 | throw_unless(38, check_signature(hash, signature, public_key.preload_uint(256))); 44 | 45 | int mask = (1 << i); 46 | int old_cnt_bits = cnt_bits; 47 | cnt_bits |= mask; 48 | int should_check = cnt_bits != old_cnt_bits; 49 | cnt -= should_check; ;; TODO: code can be simplified 50 | } until (cell_null?(signatures)); 51 | 52 | return (cnt, cnt_bits); 53 | } 54 | 55 | () recv_internal(slice in_msg) impure { 56 | ;; do nothing for internal messages 57 | } 58 | 59 | (int, int, int, slice) unpack_query_data(slice in_msg, int n, slice query, var found?, int root_i) impure inline_ref { 60 | if (found?) { 61 | throw_unless(35, query~load_int(1)); 62 | (int creator_i, int cnt, int cnt_bits, slice msg) = (query~load_uint(8), query~load_uint(8), query~load_uint(n), query); 63 | throw_unless(36, slice_hash(msg) == slice_hash(in_msg)); 64 | return (creator_i, cnt, cnt_bits, msg); 65 | } 66 | throw_unless(43, slice_refs(in_msg) * 8 == slice_bits(in_msg)); 67 | return (root_i, 0, 0, in_msg); 68 | } 69 | 70 | (cell, ()) dec_flood(cell owner_infos, int creator_i) { 71 | (slice owner_info, var found?) = owner_infos.udict_get?(8, creator_i); 72 | (int public_key, int flood) = unpack_owner_info(owner_info); 73 | owner_infos~udict_set_builder(8, creator_i, pack_owner_info(public_key, flood - 1)); 74 | return (owner_infos, ()); 75 | } 76 | 77 | () try_init() impure inline_ref { 78 | ;; first query without signatures is always accepted 79 | (int wallet_id, int n, int k, int last_cleaned, cell owner_infos, cell pending_queries) = unpack_state(); 80 | throw_if(37, last_cleaned); 81 | accept_message(); 82 | set_data(pack_state(pending_queries, owner_infos, 1, k, n, wallet_id)); 83 | } 84 | 85 | (cell, cell) update_pending_queries(cell pending_queries, cell owner_infos, slice msg, int query_id, int creator_i, int cnt, int cnt_bits, int n, int k) impure inline_ref { 86 | if (cnt >= k) { 87 | accept_message(); 88 | while (msg.slice_refs()) { 89 | var mode = msg~load_uint(8); 90 | send_raw_message(msg~load_ref(), mode); 91 | } 92 | pending_queries~udict_set_builder(64, query_id, begin_cell().store_int(0, 1)); 93 | owner_infos~dec_flood(creator_i); 94 | } else { 95 | pending_queries~udict_set_builder(64, query_id, begin_cell() 96 | .store_uint(1, 1) ;; TODO (cosmetic): active_flag is stored in different ways, need to use `store_int` 97 | .store_uint(creator_i, 8) 98 | .store_uint(cnt, 8) 99 | .store_uint(cnt_bits, n) 100 | .store_slice(msg)); 101 | } 102 | return (pending_queries, owner_infos); 103 | } 104 | 105 | (int, int) calc_boc_size(int cells, int bits, slice root) { 106 | cells += 1; 107 | bits += root.slice_bits(); 108 | 109 | while (root.slice_refs()) { 110 | (cells, bits) = calc_boc_size(cells, bits, root~load_ref().begin_parse()); 111 | } 112 | 113 | return (cells, bits); 114 | } 115 | 116 | () recv_external(slice in_msg) impure { 117 | ;; empty message triggers init 118 | if (slice_empty?(in_msg)) { 119 | return try_init(); 120 | } 121 | 122 | ;; Check root signature 123 | slice root_signature = in_msg~load_bits(512); 124 | int root_hash = slice_hash(in_msg); 125 | int root_i = in_msg~load_uint(8); 126 | 127 | (int wallet_id, int n, int k, int last_cleaned, cell owner_infos, cell pending_queries) = unpack_state(); 128 | last_cleaned -= last_cleaned == 0; ;; TODO: code can be simplified 129 | 130 | (slice owner_info, var found?) = owner_infos.udict_get?(8, root_i); 131 | throw_unless(31, found?); 132 | (int public_key, int flood) = unpack_owner_info(owner_info); 133 | throw_unless(32, check_signature(root_hash, root_signature, public_key)); 134 | 135 | cell signatures = in_msg~load_dict(); 136 | 137 | var hash = slice_hash(in_msg); 138 | int query_wallet_id = in_msg~load_uint(32); 139 | throw_unless(42, query_wallet_id == wallet_id); 140 | 141 | int query_id = in_msg~load_uint(64); 142 | 143 | (int msg_cnt, int msg_bits) = calc_boc_size(0, 0, in_msg); 144 | throw_if(40, (msg_cnt > 8) | (msg_bits > 2048)); 145 | 146 | (slice query, var found?) = pending_queries.udict_get?(64, query_id); 147 | 148 | ifnot (found?) { 149 | flood += 1; 150 | throw_if(39, flood > 10); 151 | } 152 | 153 | var bound = (now() << 32); 154 | throw_if(33, query_id < bound); 155 | 156 | (int creator_i, int cnt, int cnt_bits, slice msg) = unpack_query_data(in_msg, n, query, found?, root_i); 157 | int mask = 1 << root_i; 158 | throw_if(34, cnt_bits & mask); 159 | cnt_bits |= mask; 160 | cnt += 1; 161 | 162 | throw_if(41, ~ found? & (cnt < k) & (bound + ((60 * 60) << 32) > query_id)); 163 | 164 | set_gas_limit(100000); 165 | 166 | ifnot (found?) { 167 | owner_infos~udict_set_builder(8, root_i, pack_owner_info(public_key, flood)); 168 | } 169 | 170 | (pending_queries, owner_infos) = update_pending_queries(pending_queries, owner_infos, msg, query_id, creator_i, cnt, cnt_bits, n, k); 171 | set_data(pack_state(pending_queries, owner_infos, last_cleaned, k, n, wallet_id)); 172 | 173 | commit(); 174 | 175 | int need_save = 0; 176 | ifnot (cell_null?(signatures) | (cnt >= k)) { 177 | (int new_cnt, cnt_bits) = check_signatures(owner_infos, signatures, hash, cnt_bits); 178 | cnt += new_cnt; 179 | (pending_queries, owner_infos) = update_pending_queries(pending_queries, owner_infos, msg, query_id, creator_i, cnt, cnt_bits, n, k); 180 | need_save = -1; 181 | } 182 | 183 | accept_message(); 184 | bound -= (64 << 32); ;; clean up records expired more than 64 seconds ago 185 | int old_last_cleaned = last_cleaned; 186 | do { 187 | var (pending_queries', i, query, f) = pending_queries.udict_delete_get_min(64); 188 | f~touch(); 189 | if (f) { 190 | f = (i < bound); 191 | } 192 | if (f) { 193 | if (query~load_int(1)) { 194 | owner_infos~dec_flood(query~load_uint(8)); 195 | } 196 | pending_queries = pending_queries'; 197 | last_cleaned = i; 198 | need_save = -1; 199 | } 200 | } until (~ f); 201 | 202 | if (need_save) { 203 | set_data(pack_state(pending_queries, owner_infos, last_cleaned, k, n, wallet_id)); 204 | } 205 | } 206 | 207 | ;; Get methods 208 | ;; returns -1 for processed queries, 0 for unprocessed, 1 for unknown (forgotten) 209 | (int, int) get_query_state(int query_id) method_id { 210 | (_, int n, _, int last_cleaned, _, cell pending_queries) = unpack_state(); 211 | (slice cs, var found) = pending_queries.udict_get?(64, query_id); 212 | if (found) { 213 | if (cs~load_int(1)) { 214 | cs~load_uint(8 + 8); 215 | return (0, cs~load_uint(n)); 216 | } else { 217 | return (-1, 0); 218 | } 219 | } else { 220 | return (-(query_id <= last_cleaned), 0); 221 | } 222 | } 223 | 224 | int processed?(int query_id) method_id { 225 | (int x, _) = get_query_state(query_id); 226 | return x; 227 | } 228 | 229 | cell create_init_state(int wallet_id, int n, int k, cell owners_info) method_id { 230 | return pack_state(new_dict(), owners_info, 0, k, n, wallet_id); 231 | } 232 | 233 | cell merge_list(cell a, cell b) { 234 | if (cell_null?(a)) { 235 | return b; 236 | } 237 | if (cell_null?(b)) { 238 | return a; 239 | } 240 | slice as = a.begin_parse(); 241 | if (as.slice_refs() != 0) { 242 | cell tail = merge_list(as~load_ref(), b); 243 | return begin_cell().store_slice(as).store_ref(tail).end_cell(); 244 | } 245 | 246 | as~skip_last_bits(1); 247 | ;; as~skip_bits(1); 248 | return begin_cell().store_slice(as).store_dict(b).end_cell(); 249 | 250 | } 251 | 252 | cell get_public_keys() method_id { 253 | (_, _, _, _, cell public_keys, _) = unpack_state(); 254 | return public_keys; 255 | } 256 | 257 | (int, int) check_query_signatures(cell query) method_id { 258 | slice cs = query.begin_parse(); 259 | slice root_signature = cs~load_bits(512); 260 | int root_hash = slice_hash(cs); 261 | int root_i = cs~load_uint(8); 262 | 263 | cell public_keys = get_public_keys(); 264 | (slice public_key, var found?) = public_keys.udict_get?(8, root_i); 265 | throw_unless(31, found?); 266 | throw_unless(32, check_signature(root_hash, root_signature, public_key.preload_uint(256))); 267 | 268 | int mask = 1 << root_i; 269 | 270 | cell signatures = cs~load_dict(); 271 | if (cell_null?(signatures)) { 272 | return (1, mask); 273 | } 274 | (int cnt, mask) = check_signatures(public_keys, signatures, slice_hash(cs), mask); 275 | return (cnt + 1, mask); 276 | } 277 | 278 | cell messages_by_mask(int mask, int inv) method_id { 279 | (_, int n, _, _, _, cell pending_queries) = unpack_state(); 280 | int i = -1; 281 | cell a = new_dict(); 282 | do { 283 | (i, var cs, var f) = pending_queries.udict_get_next?(64, i); 284 | if (f) { 285 | if (cs~load_int(1)) { 286 | int cnt_bits = cs.skip_bits(8 + 8).preload_uint(n) ^ inv; 287 | if (cnt_bits & mask) { 288 | a~udict_set_builder(64, i, begin_cell().store_slice(cs)); 289 | } 290 | } 291 | } 292 | } until (~ f); 293 | return a; 294 | } 295 | 296 | cell get_messages_signed_by_id(int id) method_id { 297 | return messages_by_mask(1 << id, 0); 298 | } 299 | 300 | cell get_messages_unsigned_by_id(int id) method_id { 301 | return messages_by_mask(1 << id, ~ 0); 302 | } 303 | 304 | cell get_messages_unsigned() method_id { 305 | return messages_by_mask(~ 0, 0); 306 | } 307 | 308 | (int, int) get_n_k() method_id { 309 | (_, int n, int k, _, _, _) = unpack_state(); 310 | return (n, k); 311 | } 312 | 313 | cell merge_inner_queries(cell a, cell b) method_id { 314 | slice ca = a.begin_parse(); 315 | slice cb = b.begin_parse(); 316 | cell list_a = ca~load_dict(); 317 | cell list_b = cb~load_dict(); 318 | throw_unless(31, slice_hash(ca) == slice_hash(cb)); 319 | return begin_cell() 320 | .store_dict(merge_list(list_a, list_b)) 321 | .store_slice(ca) 322 | .end_cell(); 323 | } 324 | -------------------------------------------------------------------------------- /contracts/nft-collection.fc: -------------------------------------------------------------------------------- 1 | ;; NFT collection smart contract 2 | 3 | ;; storage scheme 4 | ;; default#_ royalty_factor:uint16 royalty_base:uint16 royalty_address:MsgAddress = RoyaltyParams; 5 | ;; storage#_ owner_address:MsgAddress next_item_index:uint64 6 | ;; ^[collection_content:^Cell common_content:^Cell] 7 | ;; nft_item_code:^Cell 8 | ;; royalty_params:^RoyaltyParams 9 | ;; = Storage; 10 | 11 | (slice, int, cell, cell, cell) load_data() inline { 12 | var ds = get_data().begin_parse(); 13 | return 14 | (ds~load_msg_addr(), ;; owner_address 15 | ds~load_uint(64), ;; next_item_index 16 | ds~load_ref(), ;; content 17 | ds~load_ref(), ;; nft_item_code 18 | ds~load_ref() ;; royalty_params 19 | ); 20 | } 21 | 22 | () save_data(slice owner_address, int next_item_index, cell content, cell nft_item_code, cell royalty_params) impure inline { 23 | set_data(begin_cell() 24 | .store_slice(owner_address) 25 | .store_uint(next_item_index, 64) 26 | .store_ref(content) 27 | .store_ref(nft_item_code) 28 | .store_ref(royalty_params) 29 | .end_cell()); 30 | } 31 | 32 | cell calculate_nft_item_state_init(int item_index, cell nft_item_code) { 33 | cell data = begin_cell().store_uint(item_index, 64).store_slice(my_address()).end_cell(); 34 | return begin_cell().store_uint(0, 2).store_dict(nft_item_code).store_dict(data).store_uint(0, 1).end_cell(); 35 | } 36 | 37 | slice calculate_nft_item_address(int wc, cell state_init) { 38 | return begin_cell().store_uint(4, 3) 39 | .store_int(wc, 8) 40 | .store_uint(cell_hash(state_init), 256) 41 | .end_cell() 42 | .begin_parse(); 43 | } 44 | 45 | () deploy_nft_item(int item_index, cell nft_item_code, int amount, cell nft_content) impure { 46 | cell state_init = calculate_nft_item_state_init(item_index, nft_item_code); 47 | slice nft_address = calculate_nft_item_address(workchain(), state_init); 48 | var msg = begin_cell() 49 | .store_uint(0x18, 6) 50 | .store_slice(nft_address) 51 | .store_coins(amount) 52 | .store_uint(4 + 2 + 1, 1 + 4 + 4 + 64 + 32 + 1 + 1 + 1) 53 | .store_ref(state_init) 54 | .store_ref(nft_content); 55 | send_raw_message(msg.end_cell(), 1); ;; pay transfer fees separately, revert on errors 56 | } 57 | 58 | () send_royalty_params(slice to_address, int query_id, slice data) impure inline { 59 | var msg = begin_cell() 60 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool packages:MsgAddress -> 011000 61 | .store_slice(to_address) 62 | .store_coins(0) 63 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 64 | .store_uint(op::report_royalty_params(), 32) 65 | .store_uint(query_id, 64) 66 | .store_slice(data); 67 | send_raw_message(msg.end_cell(), 64); ;; carry all the remaining value of the inbound message 68 | } 69 | 70 | () recv_internal(cell in_msg_full, slice in_msg_body) impure { 71 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 72 | return (); 73 | } 74 | slice cs = in_msg_full.begin_parse(); 75 | int flags = cs~load_uint(4); 76 | 77 | if (flags & 1) { ;; ignore all bounced messages 78 | return (); 79 | } 80 | slice sender_address = cs~load_msg_addr(); 81 | 82 | int op = in_msg_body~load_uint(32); 83 | int query_id = in_msg_body~load_uint(64); 84 | 85 | var (owner_address, next_item_index, content, nft_item_code, royalty_params) = load_data(); 86 | 87 | if (op == op::get_royalty_params()) { 88 | send_royalty_params(sender_address, query_id, royalty_params.begin_parse()); 89 | return (); 90 | } 91 | 92 | throw_unless(401, equal_slices(sender_address, owner_address)); 93 | 94 | 95 | if (op == 1) { ;; deploy new nft 96 | int item_index = in_msg_body~load_uint(64); 97 | throw_unless(402, item_index <= next_item_index); 98 | var is_last = item_index == next_item_index; 99 | deploy_nft_item(item_index, nft_item_code, in_msg_body~load_coins(), in_msg_body~load_ref()); 100 | if (is_last) { 101 | next_item_index += 1; 102 | save_data(owner_address, next_item_index, content, nft_item_code, royalty_params); 103 | } 104 | return (); 105 | } 106 | if (op == 2) { ;; batch deploy of new nfts 107 | int counter = 0; 108 | cell deploy_list = in_msg_body~load_ref(); 109 | do { 110 | var (item_index, item, f?) = deploy_list~udict::delete_get_min(64); 111 | if (f?) { 112 | counter += 1; 113 | if (counter >= 250) { ;; Limit due to limits of action list size 114 | throw(399); 115 | } 116 | 117 | throw_unless(403 + counter, item_index <= next_item_index); 118 | deploy_nft_item(item_index, nft_item_code, item~load_coins(), item~load_ref()); 119 | if (item_index == next_item_index) { 120 | next_item_index += 1; 121 | } 122 | } 123 | } until ( ~ f?); 124 | save_data(owner_address, next_item_index, content, nft_item_code, royalty_params); 125 | return (); 126 | } 127 | if (op == 3) { ;; change owner 128 | slice new_owner = in_msg_body~load_msg_addr(); 129 | save_data(new_owner, next_item_index, content, nft_item_code, royalty_params); 130 | return (); 131 | } 132 | throw(0xffff); 133 | } 134 | 135 | ;; Get methods 136 | 137 | (int, cell, slice) get_collection_data() method_id { 138 | var (owner_address, next_item_index, content, _, _) = load_data(); 139 | slice cs = content.begin_parse(); 140 | return (next_item_index, cs~load_ref(), owner_address); 141 | } 142 | 143 | slice get_nft_address_by_index(int index) method_id { 144 | var (_, _, _, nft_item_code, _) = load_data(); 145 | cell state_init = calculate_nft_item_state_init(index, nft_item_code); 146 | return calculate_nft_item_address(0, state_init); 147 | } 148 | 149 | (int, int, slice) royalty_params() method_id { 150 | var (_, _, _, _, royalty) = load_data(); 151 | slice rs = royalty.begin_parse(); 152 | return (rs~load_uint(16), rs~load_uint(16), rs~load_msg_addr()); 153 | } 154 | 155 | cell get_nft_content(int index, cell individual_nft_content) method_id { 156 | var (_, _, content, _, _) = load_data(); 157 | slice cs = content.begin_parse(); 158 | cs~load_ref(); 159 | slice common_content = cs~load_ref().begin_parse(); 160 | return (begin_cell() 161 | .store_uint(1, 8) ;; offchain tag 162 | .store_slice(common_content) 163 | .store_ref(individual_nft_content) 164 | .end_cell()); 165 | } -------------------------------------------------------------------------------- /contracts/nft-fixprice-sale-v3.fc: -------------------------------------------------------------------------------- 1 | ;; NFT sale smart contract v3 2 | 3 | int min_gas_amount() asm "1000000000 PUSHINT"; ;; 1 TON 4 | 5 | _ load_data() inline { 6 | var ds = get_data().begin_parse(); 7 | return ( 8 | ds~load_uint(1), ;; is_complete 9 | ds~load_uint(32), ;; created_at 10 | ds~load_msg_addr(), ;; marketplace_address 11 | ds~load_msg_addr(), ;; nft_address 12 | ds~load_msg_addr(), ;; nft_owner_address 13 | ds~load_coins(), ;; full_price 14 | ds~load_ref(), ;; fees_cell 15 | ds~load_uint(1) ;; can_deploy_by_external 16 | ); 17 | } 18 | 19 | _ load_fees(cell fees_cell) inline { 20 | var ds = fees_cell.begin_parse(); 21 | return ( 22 | ds~load_msg_addr(), ;; marketplace_fee_address 23 | ds~load_coins(), ;; marketplace_fee, 24 | ds~load_msg_addr(), ;; royalty_address 25 | ds~load_coins() ;; royalty_amount 26 | ); 27 | } 28 | 29 | () save_data(int is_complete, int created_at, slice marketplace_address, slice nft_address, slice nft_owner_address, int full_price, cell fees_cell) impure inline { 30 | set_data( 31 | begin_cell() 32 | .store_uint(is_complete, 1) 33 | .store_uint(created_at, 32) 34 | .store_slice(marketplace_address) 35 | .store_slice(nft_address) 36 | .store_slice(nft_owner_address) 37 | .store_coins(full_price) 38 | .store_ref(fees_cell) 39 | .store_uint(0, 1) ;; can_deploy_by_external 40 | .end_cell() 41 | ); 42 | } 43 | 44 | () send_money(slice address, int amount) impure inline { 45 | var msg = begin_cell() 46 | .store_uint(0x10, 6) ;; nobounce 47 | .store_slice(address) 48 | .store_coins(amount) 49 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 50 | .end_cell(); 51 | 52 | send_raw_message(msg, 1); 53 | } 54 | 55 | () buy(var args) impure { 56 | 57 | ( 58 | int created_at, 59 | slice marketplace_address, 60 | slice nft_address, 61 | slice nft_owner_address, 62 | int full_price, 63 | cell fees_cell, 64 | 65 | int my_balance, 66 | int msg_value, 67 | slice sender_address, 68 | int query_id 69 | ) = args; 70 | 71 | throw_unless(450, msg_value >= full_price + min_gas_amount()); 72 | 73 | var ( 74 | marketplace_fee_address, 75 | marketplace_fee, 76 | royalty_address, 77 | royalty_amount 78 | ) = load_fees(fees_cell); 79 | 80 | ;; Owner message 81 | send_money( 82 | nft_owner_address, 83 | full_price - marketplace_fee - royalty_amount + (my_balance - msg_value) 84 | ); 85 | 86 | ;; Royalty message 87 | if ((royalty_amount > 0) & (royalty_address.slice_bits() > 2)) { 88 | send_money( 89 | royalty_address, 90 | royalty_amount 91 | ); 92 | } 93 | 94 | ;; Marketplace fee message 95 | send_money( 96 | marketplace_fee_address, 97 | marketplace_fee 98 | ); 99 | 100 | builder nft_transfer = begin_cell() 101 | .store_uint(op::transfer(), 32) 102 | .store_uint(query_id, 64) 103 | .store_slice(sender_address) ;; new_owner_address 104 | .store_slice(sender_address) ;; response_address 105 | .store_int(0, 1) ;; empty custom_payload 106 | .store_coins(30000000) ;; forward amount to new_owner_address 0.03 TON 107 | .store_int(0, 1); ;; empty forward_payload 108 | var nft_msg = begin_cell() 109 | .store_uint(0x18, 6) 110 | .store_slice(nft_address) 111 | .store_coins(0) 112 | .store_uint(1, 1 + 4 + 4 + 64 + 32 + 1 + 1) 113 | .store_ref(nft_transfer.end_cell()); 114 | 115 | 116 | send_raw_message(nft_msg.end_cell(), 128); 117 | 118 | ;; Set sale as complete 119 | save_data( 120 | 1, 121 | created_at, 122 | marketplace_address, 123 | nft_address, 124 | nft_owner_address, 125 | full_price, 126 | fees_cell 127 | ); 128 | } 129 | 130 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 131 | slice cs = in_msg_full.begin_parse(); 132 | int flags = cs~load_uint(4); 133 | 134 | if (flags & 1) { ;; ignore all bounced messages 135 | return (); 136 | } 137 | 138 | slice sender_address = cs~load_msg_addr(); 139 | 140 | var ( 141 | is_complete, 142 | created_at, 143 | marketplace_address, 144 | nft_address, 145 | nft_owner_address, 146 | full_price, 147 | fees_cell, 148 | can_deploy_by_external 149 | ) = load_data(); 150 | 151 | int op = 0; 152 | int query_id = 0; 153 | 154 | if (in_msg_body.slice_empty?() == false) { 155 | op = in_msg_body~load_uint(32); 156 | query_id = in_msg_body~load_uint(64); 157 | } 158 | 159 | ;; alow cancel complete contract for fix bug with duplicate transfet nft to sale 160 | if (op == 3) { ;; cancel sale 161 | throw_unless(457, msg_value >= min_gas_amount()); 162 | throw_unless(458, equal_slices(sender_address, nft_owner_address) | equal_slices(sender_address, marketplace_address)); 163 | 164 | var msg = begin_cell() 165 | .store_uint(0x10, 6) ;; nobounce 166 | .store_slice(nft_address) 167 | .store_coins(0) 168 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 169 | .store_uint(op::transfer(), 32) 170 | .store_uint(query_id, 64) 171 | .store_slice(nft_owner_address) ;; new_owner_address 172 | .store_slice(nft_owner_address) ;; response_address; 173 | .store_int(0, 1) ;; empty custom_payload 174 | .store_coins(0) ;; forward amount to new_owner_address 175 | .store_int(0, 1); ;; empty forward_payload 176 | 177 | send_raw_message(msg.end_cell(), 64); 178 | 179 | save_data( 180 | 1, 181 | created_at, 182 | marketplace_address, 183 | nft_address, 184 | nft_owner_address, 185 | full_price, 186 | fees_cell 187 | ); 188 | return (); 189 | } 190 | 191 | if ((op == 555) & (is_complete == 1) & equal_slices(sender_address, marketplace_address)) { 192 | ;; way to fix unexpected troubles with sale contract 193 | ;; for example if some one transfer nft to this contract 194 | var msg = in_msg_body~load_ref().begin_parse(); 195 | var mode = msg~load_uint(8); 196 | send_raw_message(msg~load_ref(), mode); 197 | return (); 198 | } 199 | 200 | ;; Throw if sale is complete 201 | throw_if(404, is_complete == 1); 202 | 203 | var is_initialized = nft_owner_address.slice_bits() > 2; ;; not initialized if null address 204 | 205 | if (~ is_initialized) { 206 | 207 | if (equal_slices(sender_address, marketplace_address)) { 208 | return (); ;; just accept coins on deploy 209 | } 210 | 211 | throw_unless(500, equal_slices(sender_address, nft_address)); 212 | throw_unless(501, op == op::ownership_assigned()); 213 | slice prev_owner_address = in_msg_body~load_msg_addr(); 214 | 215 | save_data( 216 | is_complete, 217 | created_at, 218 | marketplace_address, 219 | nft_address, 220 | prev_owner_address, 221 | full_price, 222 | fees_cell 223 | ); 224 | 225 | return (); 226 | } 227 | 228 | if (op == 0) { 229 | buy( 230 | created_at, 231 | marketplace_address, 232 | nft_address, 233 | nft_owner_address, 234 | full_price, 235 | fees_cell, 236 | 237 | my_balance, 238 | msg_value, 239 | sender_address, 240 | 0 241 | ); 242 | return (); 243 | } 244 | 245 | if (op == 1) { ;; just accept coins 246 | return (); 247 | } 248 | 249 | if (op == 2) { ;; buy 250 | buy( 251 | created_at, 252 | marketplace_address, 253 | nft_address, 254 | nft_owner_address, 255 | full_price, 256 | fees_cell, 257 | 258 | my_balance, 259 | msg_value, 260 | sender_address, 261 | query_id 262 | ); 263 | return (); 264 | } 265 | 266 | throw(0xffff); 267 | } 268 | 269 | () recv_external(slice in_msg) impure { 270 | var ( 271 | is_complete, 272 | created_at, 273 | marketplace_address, 274 | nft_address, 275 | nft_owner_address, 276 | full_price, 277 | fees_cell, 278 | can_deploy_by_external 279 | ) = load_data(); 280 | 281 | if (can_deploy_by_external == 1) { 282 | accept_message(); 283 | save_data( 284 | is_complete, 285 | created_at, 286 | marketplace_address, 287 | nft_address, 288 | nft_owner_address, 289 | full_price, 290 | fees_cell 291 | ); 292 | return (); 293 | } 294 | throw(0xfffe); 295 | } 296 | 297 | (int, int, int, slice, slice, slice, int, slice, int, slice, int) get_sale_data() method_id { 298 | var ( 299 | is_complete, 300 | created_at, 301 | marketplace_address, 302 | nft_address, 303 | nft_owner_address, 304 | full_price, 305 | fees_cell, 306 | can_deploy_by_external 307 | ) = load_data(); 308 | 309 | var ( 310 | marketplace_fee_address, 311 | marketplace_fee, 312 | royalty_address, 313 | royalty_amount 314 | ) = load_fees(fees_cell); 315 | 316 | return ( 317 | 0x46495850, ;; fix price sale ("FIXP") 318 | is_complete == 1, 319 | created_at, 320 | marketplace_address, 321 | nft_address, 322 | nft_owner_address, 323 | full_price, 324 | marketplace_fee_address, 325 | marketplace_fee, 326 | royalty_address, 327 | royalty_amount 328 | ); 329 | } -------------------------------------------------------------------------------- /contracts/nft-item.fc: -------------------------------------------------------------------------------- 1 | ;; 2 | ;; TON NFT Item Smart Contract 3 | ;; 4 | 5 | {- 6 | 7 | NOTE that this tokens can be transferred within the same workchain. 8 | 9 | This is suitable for most tokens, if you need tokens transferable between workchains there are two solutions: 10 | 11 | 1) use more expensive but universal function below to calculate message forward fee for arbitrary destination (see `misc/forward-fee-calc.cs`) 12 | 13 | 2) use token holder proxies in target workchain (that way even 'non-universal' token can be used from any workchain) 14 | 15 | -} 16 | 17 | int min_tons_for_storage() asm "50000000 PUSHINT"; ;; 0.05 TON 18 | 19 | ;; 20 | ;; Storage 21 | ;; 22 | ;; uint64 index 23 | ;; MsgAddressInt collection_address 24 | ;; MsgAddressInt owner_address 25 | ;; cell content 26 | ;; 27 | 28 | (int, int, slice, slice, cell) load_data() { 29 | slice ds = get_data().begin_parse(); 30 | var (index, collection_address) = (ds~load_uint(64), ds~load_msg_addr()); 31 | if (ds.slice_bits() > 0) { 32 | return (-1, index, collection_address, ds~load_msg_addr(), ds~load_ref()); 33 | } else { 34 | return (0, index, collection_address, null(), null()); ;; nft not initialized yet 35 | } 36 | } 37 | 38 | () store_data(int index, slice collection_address, slice owner_address, cell content) impure { 39 | set_data( 40 | begin_cell() 41 | .store_uint(index, 64) 42 | .store_slice(collection_address) 43 | .store_slice(owner_address) 44 | .store_ref(content) 45 | .end_cell() 46 | ); 47 | } 48 | 49 | () send_msg(slice to_address, int amount, int op, int query_id, builder payload, int send_mode) impure inline { 50 | var msg = begin_cell() 51 | .store_uint(0x10, 6) ;; nobounce - int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool packages:MsgAddress -> 011000 52 | .store_slice(to_address) 53 | .store_coins(amount) 54 | .store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1) 55 | .store_uint(op, 32) 56 | .store_uint(query_id, 64); 57 | 58 | if (~ builder_null?(payload)) { 59 | msg = msg.store_builder(payload); 60 | } 61 | 62 | send_raw_message(msg.end_cell(), send_mode); 63 | } 64 | 65 | () transfer_ownership(int my_balance, int index, slice collection_address, slice owner_address, cell content, slice sender_address, int query_id, slice in_msg_body, int fwd_fees) impure inline { 66 | throw_unless(401, equal_slices(sender_address, owner_address)); 67 | 68 | slice new_owner_address = in_msg_body~load_msg_addr(); 69 | force_chain(new_owner_address); 70 | slice response_destination = in_msg_body~load_msg_addr(); 71 | in_msg_body~load_int(1); ;; this nft don't use custom_payload 72 | int forward_amount = in_msg_body~load_coins(); 73 | 74 | int rest_amount = my_balance - min_tons_for_storage(); 75 | if (forward_amount) { 76 | rest_amount -= (forward_amount + fwd_fees); 77 | } 78 | int need_response = response_destination.preload_uint(2) != 0; ;; if NOT addr_none: 00 79 | if (need_response) { 80 | rest_amount -= fwd_fees; 81 | } 82 | 83 | throw_unless(402, rest_amount >= 0); ;; base nft spends fixed amount of gas, will not check for response 84 | 85 | if (forward_amount) { 86 | send_msg(new_owner_address, forward_amount, op::ownership_assigned(), query_id, begin_cell().store_slice(owner_address).store_slice(in_msg_body), 1); ;; paying fees, revert on errors 87 | } 88 | if (need_response) { 89 | force_chain(response_destination); 90 | send_msg(response_destination, rest_amount, op::excesses(), query_id, null(), 1); ;; paying fees, revert on errors 91 | } 92 | 93 | store_data(index, collection_address, new_owner_address, content); 94 | } 95 | 96 | () recv_internal(int my_balance, int msg_value, cell in_msg_full, slice in_msg_body) impure { 97 | if (in_msg_body.slice_empty?()) { ;; ignore empty messages 98 | return (); 99 | } 100 | 101 | slice cs = in_msg_full.begin_parse(); 102 | int flags = cs~load_uint(4); 103 | 104 | if (flags & 1) { ;; ignore all bounced messages 105 | return (); 106 | } 107 | slice sender_address = cs~load_msg_addr(); 108 | 109 | cs~load_msg_addr(); ;; skip dst 110 | cs~load_coins(); ;; skip value 111 | cs~skip_bits(1); ;; skip extracurrency collection 112 | cs~load_coins(); ;; skip ihr_fee 113 | int fwd_fee = cs~load_coins(); ;; we use message fwd_fee for estimation of forward_payload costs 114 | 115 | 116 | (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data(); 117 | if (~ init?) { 118 | throw_unless(405, equal_slices(collection_address, sender_address)); 119 | store_data(index, collection_address, in_msg_body~load_msg_addr(), in_msg_body~load_ref()); 120 | return (); 121 | } 122 | 123 | int op = in_msg_body~load_uint(32); 124 | int query_id = in_msg_body~load_uint(64); 125 | 126 | if (op == op::transfer()) { 127 | transfer_ownership(my_balance, index, collection_address, owner_address, content, sender_address, query_id, in_msg_body, fwd_fee); 128 | return (); 129 | } 130 | if (op == op::get_static_data()) { 131 | send_msg(sender_address, 0, op::report_static_data(), query_id, begin_cell().store_uint(index, 256).store_slice(collection_address), 64); ;; carry all the remaining value of the inbound message 132 | return (); 133 | } 134 | throw(0xffff); 135 | } 136 | 137 | ;; 138 | ;; GET Methods 139 | ;; 140 | 141 | (int, int, slice, slice, cell) get_nft_data() method_id { 142 | (int init?, int index, slice collection_address, slice owner_address, cell content) = load_data(); 143 | return (init?, index, collection_address, owner_address, content); 144 | } -------------------------------------------------------------------------------- /lesson_1/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 1 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=LJqam4eBqyE) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | * [TonTools](https://github.com/yungwine/TonTools) 10 | * [toncli](https://github.com/disintar/toncli) 11 | * [network configs](https://docs.ton.org/develop/howto/network-configs) 12 | 13 | ### Official Docs - [link](https://docs.ton.org) 14 | 15 | ## Lesson themes 16 | - General libraries review 17 | - Wallet creation, import and deployment 18 | - Contract deployment via internal message 19 | - Counter contract deployment 20 | - Internal message creation 21 | - Contract get method parsing 22 | 23 | ## Folder structure 24 | - `mnemoincs.py` - mnemonics var 25 | - `secret.py` - api key for toncenter 26 | - `wallets.py` - creation, import and deployment of the wallet 27 | - `tontools.py` - interaction with wallets using TonTools 28 | - `counter.py` - interaction with counter contract using tonsdk, pytonlib 29 | - `wallet/` - toncli project folder 30 | -------------------------------------------------------------------------------- /lesson_1/code.func: -------------------------------------------------------------------------------- 1 | () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { 2 | slice cs = in_msg.begin_parse(); 3 | int flags = cs~load_uint(4); 4 | slice sender_address = cs~load_msg_addr(); 5 | 6 | int op = in_msg_body~load_uint(32); 7 | 8 | if (op == 15) { 9 | slice ds = get_data().begin_parse(); 10 | int counter_value = ds~load_uint(32); 11 | set_data( 12 | begin_cell().store_uint(counter_value + 1, 32).store_slice(sender_address).end_cell() 13 | ); 14 | return (); 15 | } 16 | 17 | return (); 18 | } 19 | 20 | (int, slice) get_contract_storage_data() method_id { 21 | slice ds = get_data().begin_parse(); 22 | return ( 23 | ds~load_uint(32), ;; counter_value 24 | ds~load_msg_addr() ;; the most recent sender 25 | ); 26 | } -------------------------------------------------------------------------------- /lesson_1/counter.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from pytonlib import TonlibClient 4 | from tonsdk.utils import to_nano, b64str_to_bytes 5 | 6 | from wallets import get_client, import_wallet, my_mnemonics 7 | 8 | from tonsdk.boc import begin_cell, Cell 9 | 10 | 11 | 12 | async def run_get_method(client, address:str, method:str, stack: list=[]): 13 | 14 | stack = (await client.raw_run_method(address, method, stack))['stack'] 15 | 16 | return stack 17 | 18 | 19 | async def main(): 20 | 21 | payload = begin_cell().store_uint(16, 32).end_cell() 22 | 23 | wallet = import_wallet(my_mnemonics) 24 | 25 | client = await get_client() 26 | 27 | seqno = int((await run_get_method(client, address='EQB6DGcH-A8zy6U01dqPvKaeeMcnHZO-ztcditMHmNhddmMI', method='seqno', stack=[]))[0][1], 16) 28 | 29 | boc = wallet.create_transfer_message( 30 | to_addr='EQD4ziUFradzeZBLGVpDqoBE5GegeQjzIqC8fdOEXicsi35n', 31 | amount=to_nano(0.03, 'ton'), 32 | seqno=seqno, 33 | payload=payload 34 | )['message'].to_boc(False) 35 | 36 | # await client.raw_send_message(boc) 37 | 38 | stack = await run_get_method(client, address='EQD4ziUFradzeZBLGVpDqoBE5GegeQjzIqC8fdOEXicsi35n', method='get_contract_storage_data', stack=[]) 39 | 40 | address = Cell.one_from_boc(b64str_to_bytes(stack[1][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 41 | 42 | await client.close() 43 | 44 | print(stack) 45 | print(address) 46 | 47 | 48 | if __name__ == '__main__': 49 | asyncio.run(main()) 50 | -------------------------------------------------------------------------------- /lesson_1/mnemoincs.py: -------------------------------------------------------------------------------- 1 | mnemonics = ['early', 'claw', 'echo', 'energy', 'erase', 'damp', 'expire', 'brush', 'scrub', 'ripple', 'skirt', 'beach', 'club', 'believe', 'firm', 'rely', 'head', 'neck', 'doll', 'punch', 'domain', 'phone', 'render', 'dad'] 2 | 3 | if __name__ == '__main__': 4 | pass -------------------------------------------------------------------------------- /lesson_1/tontools.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from TonTools import Wallet, TonCenterClient, Contract 4 | from mnemoincs import mnemonics 5 | from secret import api_key 6 | 7 | 8 | async def main(): 9 | 10 | client = TonCenterClient(api_key) 11 | 12 | wallet = Wallet(provider=client, mnemonics=mnemonics, version='v3r2') 13 | 14 | # print(await wallet.transfer_ton('EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', 0.01, message='hello from python lessons')) 15 | 16 | print(wallet.address) 17 | 18 | print(await wallet.get_state()) 19 | 20 | contract = Contract(provider=client, address='EQD4ziUFradzeZBLGVpDqoBE5GegeQjzIqC8fdOEXicsi35n') 21 | 22 | print(await contract.run_get_method(method='get_contract_storage_data', stack=[])) 23 | 24 | 25 | if __name__ == '__main__': 26 | asyncio.run(main()) 27 | -------------------------------------------------------------------------------- /lesson_1/wallet/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonDevStudy/pyton-lessons-eng/69360457eb2441837ab956ed3073a3ceab7ba02a/lesson_1/wallet/.DS_Store -------------------------------------------------------------------------------- /lesson_1/wallet/.gitignore: -------------------------------------------------------------------------------- 1 | *.pk 2 | -------------------------------------------------------------------------------- /lesson_1/wallet/README.md: -------------------------------------------------------------------------------- 1 | # Simple wallet project 2 | 3 | Deployed by [toncli](https://github.com/disintar/toncli) 4 | 5 | ### Usage 6 | 7 | - `toncli run fift/data.fif` 8 | - `toncli run func/code.func` 9 | - `toncli deploy -n testnet -wc 0` -------------------------------------------------------------------------------- /lesson_1/wallet/fift/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonDevStudy/pyton-lessons-eng/69360457eb2441837ab956ed3073a3ceab7ba02a/lesson_1/wallet/fift/.gitkeep -------------------------------------------------------------------------------- /lesson_1/wallet/fift/data.fif: -------------------------------------------------------------------------------- 1 | "TonUtil.fif" include 2 | "Asm.fif" include 3 | 4 | "build/contract.pk" load-generate-keypair // generate key pair 5 | constant private_key // save private to constant 6 | constant public_key // save public to constant 7 | 8 | -------------------------------------------------------------------------------- /lesson_1/wallet/fift/examples/parse_get_cell.fif: -------------------------------------------------------------------------------- 1 | "Color.fif" include 2 | .s // prints stack 3 | 4 | // you will see that integer and cell slice are in stack 5 | 6 | // print seqno (it is just int in stack) 7 | ."🤗 Seqno as int from " ^green ."GET" ^reset ." method is: " 8 | 9 | swap // get seqno as int on top of stack 10 | (dump) // dump to string 11 | type // print it to output 12 | cr // print endline 13 | 14 | [-x *] [-n|-b] [-t] [-B ] [-C ] []" +cr +tab 17 | +"Creates a request to advanced wallet created by new-wallet-v3.fif, with private key loaded from file .pk " 18 | +"and address from .addr, and saves it into .boc ('wallet-query.boc' by default)" 19 | disable-digit-options generic-help-setopt 20 | "n" "--no-bounce" { false =: allow-bounce } short-long-option 21 | "Clears bounce flag" option-help 22 | "b" "--force-bounce" { true =: force-bounce } short-long-option 23 | "Forces bounce flag" option-help 24 | "x" "--extra" { $>xcc extra-cc+! } short-long-option-arg 25 | "Indicates the amount of extra currencies to be transfered" option-help 26 | "t" "--timeout" { parse-int =: timeout } short-long-option-arg 27 | "Sets expiration timeout in seconds (" timeout (.) $+ +" by default)" option-help 28 | "B" "--body" { =: body-fift-file } short-long-option-arg 29 | "Sets the payload of the transfer message" option-help 30 | "C" "--comment" { =: comment } short-long-option-arg 31 | "Sets the comment to be sent in the transfer message" option-help 32 | "m" "--mode" { parse-int =: send-mode } short-long-option-arg 33 | "Sets transfer mode (0..255) for SENDRAWMSG (" send-mode (.) $+ +" by default)" 34 | option-help 35 | "h" "--help" { usage } short-long-option 36 | "Shows a help message" option-help 37 | parse-options 38 | 39 | $# dup 5 < swap 6 > or ' usage if 40 | 6 :$1..n 41 | 42 | true constant bounce 43 | $1 =: file-base 44 | $2 bounce parse-load-address force-bounce or allow-bounce and =: bounce 2=: dest_addr 45 | $3 parse-int =: subwallet_id 46 | $4 parse-int =: seqno 47 | $5 $>cc extra-cc+! extra-currencies @ 2=: amount 48 | $6 "wallet-query" replace-if-null =: savefile 49 | subwallet_id (.) 1 ' $+ does : +subwallet 50 | 51 | file-base +subwallet +".addr" dup file-exists? { drop file-base +".addr" } ifnot 52 | load-address 53 | 2dup 2constant wallet_addr 54 | ."INFO: 👀 Source wallet address = " 2dup 6 .Addr cr 55 | file-base +".pk" load-keypair nip constant wallet_pk 56 | 57 | def? body-fift-file { @' body-fift-file include } { comment simple-transfer-body } cond 58 | constant body-cell 59 | 60 | ."INFO: 👋 Send " dest_addr 2dup bounce 7 + .Addr ." = " 61 | ."subwallet_id=0x" subwallet_id x. 62 | ."seqno=0x" seqno x. ."bounce=" bounce . cr 63 | ."INFO: 🧟 Body of transfer message is " body-cell 69 | 70 | 71 | 72 | dup hashu wallet_pk ed25519_sign_uint 73 | 75 | 76 | 2 boc+>B 77 | 78 | saveboc // it is defined in build/cli.fif (or /tmp if in script mode) 79 | // it is project-specific lib from tlcli thats specify needed locations 80 | -------------------------------------------------------------------------------- /lesson_1/wallet/func/code.func: -------------------------------------------------------------------------------- 1 | () recv_internal(int msg_value, cell in_msg, slice in_msg_body) impure { 2 | slice cs = in_msg.begin_parse(); 3 | int flags = cs~load_uint(4); 4 | slice sender_address = cs~load_msg_addr(); 5 | 6 | int op = in_msg_body~load_uint(32); 7 | 8 | if (op == 15) { 9 | slice ds = get_data().begin_parse(); 10 | int counter_value = ds~load_uint(32); 11 | set_data( 12 | begin_cell().store_uint(counter_value + 1, 32).store_slice(sender_address).end_cell() 13 | ); 14 | return (); 15 | } 16 | 17 | return (); 18 | } 19 | 20 | (int, slice) get_contract_storage_data() method_id { 21 | slice ds = get_data().begin_parse(); 22 | return ( 23 | ds~load_uint(32), ;; counter_value 24 | ds~load_msg_addr() ;; the most recent sender 25 | ); 26 | } -------------------------------------------------------------------------------- /lesson_1/wallet/project.yaml: -------------------------------------------------------------------------------- 1 | contract: 2 | data: fift/data.fif 3 | func: 4 | - func/code.func 5 | -------------------------------------------------------------------------------- /lesson_1/wallet/tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonDevStudy/pyton-lessons-eng/69360457eb2441837ab956ed3073a3ceab7ba02a/lesson_1/wallet/tests/.gitkeep -------------------------------------------------------------------------------- /lesson_1/wallets.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import asyncio 3 | import requests 4 | 5 | 6 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 7 | from tonsdk.crypto import mnemonic_new 8 | from tonsdk.utils import to_nano 9 | 10 | from mnemoincs import mnemonics as my_mnemonics 11 | from pytonlib import TonlibClient 12 | 13 | 14 | def create_wallet(): 15 | mnemonics, pub_k, priv_k, wallet = Wallets.create(WalletVersionEnum.v3r2, workchain=0) 16 | 17 | return wallet 18 | 19 | 20 | def import_wallet(mnemoincs): 21 | mnemonics, pub_k, priv_k, wallet = Wallets.from_mnemonics(mnemonics=mnemoincs) 22 | 23 | return wallet 24 | 25 | 26 | async def get_client(): 27 | ton_config = requests.get('https://ton.org/global.config.json').json() 28 | 29 | keystore_dir = '/tmp/ton_keystore' 30 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 31 | 32 | # init TonlibClient 33 | client = TonlibClient(ls_index=2, # choose LiteServer index to connect 34 | config=ton_config, 35 | keystore=keystore_dir) 36 | 37 | # init tonlibjson 38 | await client.init() 39 | 40 | return client 41 | 42 | 43 | async def deploy_wallet(): 44 | wallet = import_wallet(my_mnemonics) 45 | 46 | boc = wallet.create_init_external_message()['message'].to_boc(False) 47 | 48 | client = await get_client() 49 | 50 | await client.raw_send_message(boc) 51 | 52 | await client.close() 53 | 54 | 55 | async def deploy_wallet_internal(): 56 | 57 | new_wallet = create_wallet() 58 | 59 | state_init = new_wallet.create_state_init()['state_init'] 60 | 61 | wallet = import_wallet(my_mnemonics) 62 | 63 | boc = wallet.create_transfer_message( 64 | to_addr=new_wallet.address.to_string(), 65 | amount=to_nano(0.05, 'ton'), 66 | seqno=1, 67 | state_init=state_init 68 | )['message'].to_boc(False) 69 | 70 | client = await get_client() 71 | 72 | await client.raw_send_message(boc) 73 | 74 | await client.close() 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | asyncio.run(deploy_wallet_internal()) 80 | -------------------------------------------------------------------------------- /lesson_10/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 10 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=x4rG-aFOqNo) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | * [tontools](https://github.com/yungwine/TonTools) 10 | 11 | 12 | * [Nodes documentation](https://docs.ton.org/participate/nodes/node-types) 13 | * [mytonctrl](https://github.com/ton-blockchain/mytonctrl/) 14 | * [ton-http-api](https://github.com/toncenter/ton-http-api/) 15 | * [Blockchain dumps](https://dump.ton.org/) 16 | 17 | ### Official Docs - [link](https://docs.ton.org) 18 | 19 | ## Lesson themes 20 | - TON Node types 21 | - Launching Liteserver using mytonctrl 22 | - Launching local toncenter 23 | - interaction with your own Liteserver 24 | 25 | ## Folder structure 26 | - `client.py` - connection to own liteserver using [pytonlib](https://github.com/toncenter/pytonlib) 27 | - `config.json` - config of own liteserver 28 | - `tontools.py` - connection to local toncenter using [TonTools](https://github.com/yungwine/TonTools) 29 | -------------------------------------------------------------------------------- /lesson_10/client.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import bitarray 4 | from pytonlib import TonlibClient 5 | from tonsdk.boc import Cell 6 | 7 | import requests 8 | from pathlib import Path 9 | import json 10 | from tvm_valuetypes import deserialize_boc, parse_hashmap 11 | 12 | 13 | async def get_client() -> TonlibClient: 14 | with open('config.json', 'r') as f: 15 | config = json.loads(f.read()) 16 | 17 | keystore_dir = '/tmp/ton_keystore' 18 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 19 | 20 | client = TonlibClient(ls_index=0, config=config, keystore=keystore_dir, tonlib_timeout=10) 21 | await client.init() 22 | 23 | return client 24 | 25 | 26 | async def test_client(): 27 | client = await get_client() 28 | 29 | print(await client.get_masterchain_info()) 30 | 31 | await client.close() 32 | 33 | 34 | if __name__ == '__main__': 35 | asyncio.run(test_client()) 36 | -------------------------------------------------------------------------------- /lesson_10/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "@type": "config.global", 3 | "dht": { 4 | "@type": "dht.config.global", 5 | "k": 6, 6 | "a": 3, 7 | "static_nodes": { 8 | "@type": "dht.nodes", 9 | "nodes": [ 10 | { 11 | "@type": "dht.node", 12 | "id": { 13 | "@type": "pub.ed25519", 14 | "key": "6PGkPQSbyFp12esf1NqmDOaLoFA8i9+Mp5+cAx5wtTU=" 15 | }, 16 | "addr_list": { 17 | "@type": "adnl.addressList", 18 | "addrs": [ 19 | { 20 | "@type": "adnl.address.udp", 21 | "ip": -1185526007, 22 | "port": 22096 23 | } 24 | ], 25 | "version": 0, 26 | "reinit_date": 0, 27 | "priority": 0, 28 | "expire_at": 0 29 | }, 30 | "version": -1, 31 | "signature": "L4N1+dzXLlkmT5iPnvsmsixzXU0L6kPKApqMdcrGP5d9ssMhn69SzHFK+yIzvG6zQ9oRb4TnqPBaKShjjj2OBg==" 32 | }, 33 | { 34 | "@type": "dht.node", 35 | "id": { 36 | "@type": "pub.ed25519", 37 | "key": "4R0C/zU56k+x2HGMsLWjX2rP/SpoTPIHSSAmidGlsb8=" 38 | }, 39 | "addr_list": { 40 | "@type": "adnl.addressList", 41 | "addrs": [ 42 | { 43 | "@type": "adnl.address.udp", 44 | "ip": -1952265919, 45 | "port": 14395 46 | } 47 | ], 48 | "version": 0, 49 | "reinit_date": 0, 50 | "priority": 0, 51 | "expire_at": 0 52 | }, 53 | "version": -1, 54 | "signature": "0uwWyCFn2KjPnnlbSFYXLZdwIakaSgI9WyRo87J3iCGwb5TvJSztgA224A9kNAXeutOrXMIPYv1b8Zt8ImsrCg==" 55 | }, 56 | { 57 | "@type": "dht.node", 58 | "id": { 59 | "@type": "pub.ed25519", 60 | "key": "/YDNd+IwRUgL0mq21oC0L3RxrS8gTu0nciSPUrhqR78=" 61 | }, 62 | "addr_list": { 63 | "@type": "adnl.addressList", 64 | "addrs": [ 65 | { 66 | "@type": "adnl.address.udp", 67 | "ip": -1402455171, 68 | "port": 14432 69 | } 70 | ], 71 | "version": 0, 72 | "reinit_date": 0, 73 | "priority": 0, 74 | "expire_at": 0 75 | }, 76 | "version": -1, 77 | "signature": "6+oVk6HDtIFbwYi9khCc8B+fTFceBUo1PWZDVTkb4l84tscvr5QpzAkdK7sS5xGzxM7V7YYQ6gUQPrsP9xcLAw==" 78 | }, 79 | { 80 | "@type": "dht.node", 81 | "id": { 82 | "@type": "pub.ed25519", 83 | "key": "DA0H568bb+LoO2LGY80PgPee59jTPCqqSJJzt1SH+KE=" 84 | }, 85 | "addr_list": { 86 | "@type": "adnl.addressList", 87 | "addrs": [ 88 | { 89 | "@type": "adnl.address.udp", 90 | "ip": -1402397332, 91 | "port": 14583 92 | } 93 | ], 94 | "version": 0, 95 | "reinit_date": 0, 96 | "priority": 0, 97 | "expire_at": 0 98 | }, 99 | "version": -1, 100 | "signature": "cL79gDTrixhaM9AlkCdZWccCts7ieQYQBmPxb/R7d7zHw3bEHL8Le96CFJoB1KHu8C85iDpFK8qlrGl1Yt/ZDg==" 101 | }, 102 | { 103 | "@type": "dht.node", 104 | "id": { 105 | "@type": "pub.ed25519", 106 | "key": "fZnkoIAxrTd4xeBgVpZFRm5SvVvSx7eN3Vbe8c83YMk=" 107 | }, 108 | "addr_list": { 109 | "@type": "adnl.addressList", 110 | "addrs": [ 111 | { 112 | "@type": "adnl.address.udp", 113 | "ip": 1091897261, 114 | "port": 15813 115 | } 116 | ], 117 | "version": 0, 118 | "reinit_date": 0, 119 | "priority": 0, 120 | "expire_at": 0 121 | }, 122 | "version": -1, 123 | "signature": "cmaMrV/9wuaHOOyXYjoxBnckJktJqrQZ2i+YaY3ehIyiL3LkW81OQ91vm8zzsx1kwwadGZNzgq4hI4PCB/U5Dw==" 124 | }, 125 | { 126 | "@type": "dht.node", 127 | "id": { 128 | "@type": "pub.ed25519", 129 | "key": "zDBLsKjns4bBqQokzY0wOzC2vwbOeiE1J7aOjfCp5mg=" 130 | }, 131 | "addr_list": { 132 | "@type": "adnl.addressList", 133 | "addrs": [ 134 | { 135 | "@type": "adnl.address.udp", 136 | "ip": -1573440928, 137 | "port": 12821 138 | } 139 | ], 140 | "version": 0, 141 | "reinit_date": 0, 142 | "priority": 0, 143 | "expire_at": 0 144 | }, 145 | "version": -1, 146 | "signature": "qORMhem9RyG7wnNYF822YL3EXwEoTO82h2TarFbjd0jikMIGizAdir1JyxSfyKkhHdFKGcLMeoPb2dfMIvQwAA==" 147 | }, 148 | { 149 | "@type": "dht.node", 150 | "id": { 151 | "@type": "pub.ed25519", 152 | "key": "CU9ytJok8WBnpl29T740gfC/h69kgvQJp7FJMq/N60g=" 153 | }, 154 | "addr_list": { 155 | "@type": "adnl.addressList", 156 | "addrs": [ 157 | { 158 | "@type": "adnl.address.udp", 159 | "ip": 391653587, 160 | "port": 15895 161 | } 162 | ], 163 | "version": 0, 164 | "reinit_date": 0, 165 | "priority": 0, 166 | "expire_at": 0 167 | }, 168 | "version": -1, 169 | "signature": "DKyGF2nPRxmerpIHxE5FN1Lod3zvJu728NP0iYc1hpNyPvl5epu+7amjimLy1VdzNqFzTJAoJ/gqPPMkXS/kDw==" 170 | }, 171 | { 172 | "@type": "dht.node", 173 | "id": { 174 | "@type": "pub.ed25519", 175 | "key": "MJr8xja0xpu9DoisFXBrkNHNx1XozR7HHw9fJdSyEdo=" 176 | }, 177 | "addr_list": { 178 | "@type": "adnl.addressList", 179 | "addrs": [ 180 | { 181 | "@type": "adnl.address.udp", 182 | "ip": -2018147130, 183 | "port": 6302 184 | } 185 | ], 186 | "version": 0, 187 | "reinit_date": 0, 188 | "priority": 0, 189 | "expire_at": 0 190 | }, 191 | "version": -1, 192 | "signature": "XcR5JaWcf4QMdI8urLSc1zwv5+9nCuItSE1EDa0dSwYF15R/BtJoKU5YHA4/T8SiO18aVPQk2SL1pbhevuMrAQ==" 193 | }, 194 | { 195 | "@type": "dht.node", 196 | "id": { 197 | "@type": "pub.ed25519", 198 | "key": "Fhldu4zlnb20/TUj9TXElZkiEmbndIiE/DXrbGKu+0c=" 199 | }, 200 | "addr_list": { 201 | "@type": "adnl.addressList", 202 | "addrs": [ 203 | { 204 | "@type": "adnl.address.udp", 205 | "ip": -2018147075, 206 | "port": 6302 207 | } 208 | ], 209 | "version": 0, 210 | "reinit_date": 0, 211 | "priority": 0, 212 | "expire_at": 0 213 | }, 214 | "version": -1, 215 | "signature": "nUGB77UAkd2+ZAL5PgInb3TvtuLLXJEJ2icjAUKLv4qIGB3c/O9k/v0NKwSzhsMP0ljeTGbcIoMDw24qf3goCg==" 216 | }, 217 | { 218 | "@type": "dht.node", 219 | "id": { 220 | "@type": "pub.ed25519", 221 | "key": "gzUNJnBJhdpooYCE8juKZo2y4tYDIQfoCvFm0yBr7y0=" 222 | }, 223 | "addr_list": { 224 | "@type": "adnl.addressList", 225 | "addrs": [ 226 | { 227 | "@type": "adnl.address.udp", 228 | "ip": 89013260, 229 | "port": 54390 230 | } 231 | ], 232 | "version": 0, 233 | "reinit_date": 0, 234 | "priority": 0, 235 | "expire_at": 0 236 | }, 237 | "version": -1, 238 | "signature": "LCrCkjmkMn6AZHW2I+oRm1gHK7CyBPfcb6LwsltskCPpNECyBl1GxZTX45n0xZtLgyBd/bOqMPBfawpQwWt1BA==" 239 | }, 240 | { 241 | "@type": "dht.node", 242 | "id": { 243 | "@type": "pub.ed25519", 244 | "key": "jXiLaOQz1HPayilWgBWhV9xJhUIqfU95t+KFKQPIpXg=" 245 | }, 246 | "addr_list": { 247 | "@type": "adnl.addressList", 248 | "addrs": [ 249 | { 250 | "@type": "adnl.address.udp", 251 | "ip": 94452896, 252 | "port": 12485 253 | } 254 | ], 255 | "version": 0, 256 | "reinit_date": 0, 257 | "priority": 0, 258 | "expire_at": 0 259 | }, 260 | "version": -1, 261 | "signature": "fKSZh9nXMx+YblkQXn3I/bndTD0JZ1yAtK/tXPIGruNglpe9sWMXR+8fy3YogPhLJMdjNiMom1ya+tWG7qvBAQ==" 262 | }, 263 | { 264 | "@type": "dht.node", 265 | "id": { 266 | "@type": "pub.ed25519", 267 | "key": "vhFPq+tgjJi+4ZbEOHBo4qjpqhBdSCzNZBdgXyj3NK8=" 268 | }, 269 | "addr_list": { 270 | "@type": "adnl.addressList", 271 | "addrs": [ 272 | { 273 | "@type": "adnl.address.udp", 274 | "ip": 85383775, 275 | "port": 36752 276 | } 277 | ], 278 | "version": 0, 279 | "reinit_date": 0, 280 | "priority": 0, 281 | "expire_at": 0 282 | }, 283 | "version": -1, 284 | "signature": "kBwAIgJVkz8AIOGoZcZcXWgNmWq8MSBWB2VhS8Pd+f9LLPIeeFxlDTtwAe8Kj7NkHDSDC+bPXLGQZvPv0+wHCg==" 285 | }, 286 | { 287 | "@type": "dht.node", 288 | "id": { 289 | "@type": "pub.ed25519", 290 | "key": "sbsuMcdyYFSRQ0sG86/n+ZQ5FX3zOWm1aCVuHwXdgs0=" 291 | }, 292 | "addr_list": { 293 | "@type": "adnl.addressList", 294 | "addrs": [ 295 | { 296 | "@type": "adnl.address.udp", 297 | "ip": 759132846, 298 | "port": 50187 299 | } 300 | ], 301 | "version": 0, 302 | "reinit_date": 0, 303 | "priority": 0, 304 | "expire_at": 0 305 | }, 306 | "version": -1, 307 | "signature": "9FJwbFw3IECRFkb9bA54YaexjDmlNBArimWkh+BvW88mjm3K2i5V2uaBPS3GubvXWOwdHLE2lzQBobgZRGMyCg==" 308 | }, 309 | { 310 | "@type": "dht.node", 311 | "id": { 312 | "@type": "pub.ed25519", 313 | "key": "aeMgdMdkkbkfAS4+n4BEGgtqhkf2/zXrVWWECOJ/h3A=" 314 | }, 315 | "addr_list": { 316 | "@type": "adnl.addressList", 317 | "addrs": [ 318 | { 319 | "@type": "adnl.address.udp", 320 | "ip": -1481887565, 321 | "port": 25975 322 | } 323 | ], 324 | "version": 0, 325 | "reinit_date": 0, 326 | "priority": 0, 327 | "expire_at": 0 328 | }, 329 | "version": -1, 330 | "signature": "z5ogivZWpQchkS4UR4wB7i2pfOpMwX9Nd/USxinL9LvJPa+/Aw3F1AytR9FX0BqDftxIYvblBYAB5JyAmlj+AA==" 331 | }, 332 | { 333 | "@type": "dht.node", 334 | "id": { 335 | "@type": "pub.ed25519", 336 | "key": "rNzhnAlmtRn9rTzW6o2568S6bbOXly7ddO1olDws5wM=" 337 | }, 338 | "addr_list": { 339 | "@type": "adnl.addressList", 340 | "addrs": [ 341 | { 342 | "@type": "adnl.address.udp", 343 | "ip": -2134428422, 344 | "port": 45943 345 | } 346 | ], 347 | "version": 0, 348 | "reinit_date": 0, 349 | "priority": 0, 350 | "expire_at": 0 351 | }, 352 | "version": -1, 353 | "signature": "sn/+ZfkfCSw2bHnEnv04AXX/Goyw7+StHBPQOdPr+wvdbaJ761D7hyiMNdQGbuZv2Ep2cXJpiwylnZItrwdUDg==" 354 | } 355 | ] 356 | } 357 | }, 358 | "liteservers": [ 359 | { 360 | "ip": 1412837294, 361 | "port": 41937, 362 | "id": { 363 | "@type": "pub.ed25519", 364 | "key": "R9QB4gQpUQw+bjzwfnx71VT8KlpaTrweeUVnbV8BC3o=" 365 | } 366 | } 367 | ], 368 | "validator": { 369 | "@type": "validator.config.global", 370 | "zero_state": { 371 | "workchain": -1, 372 | "shard": -9223372036854775808, 373 | "seqno": 0, 374 | "root_hash": "F6OpKZKqvqeFp6CQmFomXNMfMj2EnaUSOXN+Mh+wVWk=", 375 | "file_hash": "XplPz01CXAps5qeSWUtxcyBfdAo5zVb1N979KLSKD24=" 376 | }, 377 | "init_block": { 378 | "root_hash": "PjdXRU5t6S69NdiuQ18peyd5hL8hezsSNKcR7ph2z60=", 379 | "seqno": 30153173, 380 | "file_hash": "c38ng8T5zSqSIIjd7ZdHZLyETUUXjHtC0cQsU+H+PAU=", 381 | "workchain": -1, 382 | "shard": -9223372036854775808 383 | }, 384 | "hardforks": [ 385 | { 386 | "file_hash": "t/9VBPODF7Zdh4nsnA49dprO69nQNMqYL+zk5bCjV/8=", 387 | "seqno": 8536841, 388 | "root_hash": "08Kpc9XxrMKC6BF/FeNHPS3MEL1/Vi/fQU/C9ELUrkc=", 389 | "workchain": -1, 390 | "shard": -9223372036854775808 391 | } 392 | ] 393 | } 394 | } -------------------------------------------------------------------------------- /lesson_10/tontools.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from TonTools import Contract, TonCenterClient 4 | 5 | 6 | async def main(): 7 | client = TonCenterClient(base_url='http://84.54.47.174:80/') 8 | contract = Contract(address='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', provider=client) 9 | print(await contract.get_balance()) 10 | 11 | 12 | if __name__ == '__main__': 13 | asyncio.run(main()) 14 | -------------------------------------------------------------------------------- /lesson_2/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 2 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=ipFDBjJFLCw) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | * [ton-http-api](https://github.com/toncenter/ton-http-api) 10 | * [network configs](https://docs.ton.org/develop/howto/network-configs) 11 | 12 | ### Official Docs - [link](https://docs.ton.org) 13 | 14 | ## Lesson themes 15 | - Cell creation 16 | - Slice parsing 17 | - Work with liteservers 18 | - Looking for archive ls from public ls list 19 | - Transactions parsing from blocks 20 | - Listening for new transactions in the blockchain 21 | 22 | ## Folder structure 23 | - `cells.py` - cell creation and parsing 24 | - `client.py` - initializing of TonLibClient 25 | - `liteservers.py` - looking for archive liteservers 26 | - `blocks.py` - blocks parsing; listening for new blocks 27 | -------------------------------------------------------------------------------- /lesson_2/blocks.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from client import get_client 4 | from pytonlib import TonlibClient 5 | 6 | 7 | async def get_block(wc: int, shard: int, seqno: int, client: TonlibClient): 8 | return (await client.get_block_transactions(wc, shard, seqno, count=40))['transactions'] 9 | 10 | 11 | async def get_transactions(tr_ids: list, client: TonlibClient): 12 | result = [] 13 | for tr_id in tr_ids: 14 | trs = await client.get_transactions(account=tr_id['account'], 15 | from_transaction_lt=tr_id['lt'], 16 | from_transaction_hash=tr_id['hash'], 17 | limit=1) 18 | result.append(trs[0]) 19 | return result 20 | 21 | 22 | async def last_master_blocks(client: TonlibClient): 23 | 24 | while True: 25 | master_data = (await client.get_masterchain_info())['last'] 26 | print(master_data['seqno']) 27 | trs = await get_transactions(await get_block(master_data['workchain'], master_data['shard'], master_data['seqno'], client), client) 28 | print(trs) 29 | 30 | 31 | async def last_basechain_blocks(client: TonlibClient): 32 | while True: 33 | master_data = (await client.get_masterchain_info())['last'] 34 | print(master_data['seqno']) 35 | base_data = (await client.get_shards(master_seqno=master_data['seqno']))['shards'][0] 36 | print(base_data['seqno']) 37 | trs = await get_transactions( 38 | await get_block(base_data['workchain'], base_data['shard'], base_data['seqno'], client), client) 39 | print(trs) 40 | 41 | 42 | async def main(): 43 | client = await get_client(0) 44 | 45 | # tr_ids = await get_block(-1, -9223372036854775808, 29000000, client) 46 | # print(await get_transactions(tr_ids, client)) 47 | 48 | # await last_master_blocks(client) 49 | await last_basechain_blocks(client) 50 | 51 | await client.close() 52 | 53 | 54 | if __name__ == '__main__': 55 | asyncio.run(main()) 56 | -------------------------------------------------------------------------------- /lesson_2/cells.py: -------------------------------------------------------------------------------- 1 | from tonsdk.boc import Cell 2 | from tonsdk.boc import begin_cell 3 | from tonsdk.utils import Address 4 | from bitarray import bitarray 5 | 6 | 7 | def cell_create(): 8 | cell = begin_cell()\ 9 | .store_uint(15, 32)\ 10 | .store_address(Address('EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N'))\ 11 | .store_coins(1.123 * 10*10**9)\ 12 | .end_cell() 13 | return cell 14 | 15 | 16 | def parse_cell_bitarray(): 17 | a = bitarray() 18 | cell = cell_create() 19 | a.frombytes(cell.bits.get_top_upped_array()) 20 | 21 | num = int(a[:32].to01(), 2) 22 | del a[:32] 23 | print(num) 24 | 25 | del a[:3] 26 | 27 | wc = hex(int(a[:8].to01(), 2)).replace('0x', '') 28 | 29 | del a[:8] 30 | 31 | hash_part = a[:256].tobytes().hex() 32 | del a[:256] 33 | 34 | print(wc, hash_part) 35 | 36 | print(Address(str(wc) + ':' + hash_part).to_string(True, True, True)) 37 | 38 | l = int(a[:4].to01(), 2) 39 | del a[:4] 40 | 41 | print(int(a[:l * 8].to01(), 2)) 42 | del a[:l * 8] 43 | 44 | 45 | def parse_cell_tonsdk(): 46 | cell = cell_create() 47 | 48 | slice = cell.begin_parse() 49 | 50 | num = slice.read_uint(32) 51 | 52 | address = slice.read_msg_addr() 53 | 54 | grams = slice.read_coins() 55 | 56 | print(num, address.to_string(True, True, True), grams) 57 | 58 | 59 | if __name__ == '__main__': 60 | parse_cell_bitarray() 61 | 62 | -------------------------------------------------------------------------------- /lesson_2/client.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from pathlib import Path 3 | from pytonlib import TonlibClient 4 | 5 | 6 | async def get_client(ls_index: int): 7 | ton_config = requests.get('https://ton.org/global.config.json').json() 8 | 9 | keystore_dir = '/tmp/ton_keystore' 10 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 11 | 12 | # init TonlibClient 13 | client = TonlibClient(ls_index=ls_index, # choose LiteServer index to connect 14 | config=ton_config, 15 | keystore=keystore_dir) 16 | 17 | # init tonlibjson 18 | await client.init() 19 | 20 | return client 21 | -------------------------------------------------------------------------------- /lesson_2/liteservers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import requests 4 | 5 | from client import get_client 6 | import random 7 | 8 | 9 | async def is_archival(ls_index: int): 10 | client = await get_client(ls_index) 11 | try: 12 | await client.get_block_transactions(-1, -9223372036854775808, random.randint(2, 4096), count=10) 13 | print(ls_index, 'is archival!') 14 | except: 15 | print(ls_index, 'is not archival!') 16 | await client.close() 17 | 18 | 19 | async def main(): 20 | ton_config = requests.get('https://ton.org/global.config.json').json() 21 | 22 | amount = len(ton_config['liteservers']) 23 | 24 | # client = await get_client(0) 25 | # last = (await client.get_masterchain_info())['last'] 26 | # print(last) 27 | 28 | for i in range(amount): 29 | await is_archival(i) 30 | 31 | 32 | if __name__ == '__main__': 33 | asyncio.run(main()) 34 | -------------------------------------------------------------------------------- /lesson_3/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 3 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=f3u0g84dFhY) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | * [exit codes](https://docs.ton.org/learn/tvm-instructions/tvm-exit-codes) 10 | * [transactions phases](https://docs.ton.org/learn/tvm-instructions/tvm-overview#compute-phase) 11 | * [send modes](https://docs.ton.org/develop/func/stdlib/#send_raw_message) 12 | 13 | ### Official Docs - [link](https://docs.ton.org) 14 | 15 | ## Lesson themes 16 | - Transactions data parsing 17 | - Transactions statuses 18 | - Messages parsing 19 | - Transactions types 20 | - Jetton transfer parsing 21 | - Jetton burning parsing 22 | 23 | ## Folder structure 24 | - `fail_transaction.py` - creating of unsuccessful transaction 25 | - `parse_tr_status.py` - parsing of transaction's status 26 | - `parse_tr_type.py` - parsing of transaction's type 27 | - `wallets.py` - wallet import func 28 | -------------------------------------------------------------------------------- /lesson_3/fail_transaction.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from tests.mnemoincs import mnemonics 4 | 5 | from pytonlib import TonlibClient 6 | from wallets import import_wallet, get_client 7 | 8 | 9 | async def get_seqno(client: TonlibClient, address:str): 10 | return int((await client.raw_run_method( 11 | method='seqno', 12 | address=address, 13 | stack_data=[] 14 | ))['stack'][0][1], 16) 15 | 16 | 17 | async def main(): 18 | client = await get_client(5) 19 | 20 | wallet = import_wallet(mnemonics) 21 | 22 | query = wallet.create_transfer_message( 23 | to_addr='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', 24 | amount=30 * 10**9, 25 | seqno=await get_seqno(client, address=wallet.address.to_string()), 26 | send_mode=3 27 | )['message'] 28 | 29 | await client.raw_send_message(query.to_boc(False)) 30 | 31 | await client.close() 32 | 33 | 34 | if __name__ == '__main__': 35 | asyncio.run(main()) 36 | 37 | -------------------------------------------------------------------------------- /lesson_3/parse_tr_status.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from TonTools import Contract, TonCenterClient 3 | from secret import api_key 4 | from wallets import get_client 5 | 6 | from pytonlib.utils.tlb import Transaction, deserialize_boc, Slice 7 | import base64 8 | 9 | 10 | async def tontools(): 11 | client = TonCenterClient(api_key) 12 | 13 | contract = Contract(provider=client, address='EQB6DGcH-A8zy6U01dqPvKaeeMcnHZO-ztcditMHmNhddmMI') 14 | 15 | trs = await contract.get_transactions(limit=10) 16 | 17 | for tr in trs: 18 | print(tr.to_dict_user_friendly()) 19 | 20 | 21 | async def pytonlib(): 22 | client = await get_client(5) 23 | 24 | trs = await client.get_transactions( 25 | account='EQB6DGcH-A8zy6U01dqPvKaeeMcnHZO-ztcditMHmNhddmMI', 26 | limit=10 27 | ) 28 | 29 | for tr in trs: 30 | # print(tr) 31 | 32 | cell = deserialize_boc(base64.b64decode(tr['data'])) 33 | # print(cell) 34 | 35 | tr_data = Transaction(Slice(cell)) 36 | 37 | ac_ph_code = tr_data.description.action.result_code 38 | print('action', ac_ph_code) 39 | if tr_data.description.compute_ph.type != 'tr_phase_compute_skipped': 40 | compute_ph_code = tr_data.description.compute_ph.exit_code 41 | print('compute', compute_ph_code) 42 | 43 | await client.close() 44 | 45 | 46 | if __name__ == '__main__': 47 | asyncio.run(pytonlib()) 48 | -------------------------------------------------------------------------------- /lesson_3/parse_tr_type.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from TonTools import Contract, TonCenterClient 3 | from secret import api_key 4 | from tests.wallets import get_client 5 | 6 | import base64 7 | 8 | from tonsdk.boc import Cell 9 | 10 | 11 | async def tontools(): 12 | client = TonCenterClient(api_key) 13 | 14 | contract = Contract(provider=client, address='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG') 15 | 16 | trs = await contract.get_transactions(limit=5) 17 | 18 | print(trs[4].to_dict()) 19 | 20 | 21 | # for tr in trs: 22 | # print(tr.to_dict_user_friendly()) 23 | 24 | 25 | async def pytonlib(): 26 | client = await get_client(5) 27 | 28 | trs = await client.get_transactions( 29 | account='EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG', 30 | limit=5 31 | ) 32 | 33 | for tr in trs: 34 | 35 | if tr['out_msgs']: 36 | cell = Cell.one_from_boc(base64.b64decode(tr['out_msgs'][0]['msg_data']['body'])) 37 | slice = cell.begin_parse() 38 | if len(slice) >= 32: 39 | op = slice.read_bytes(4).hex() 40 | if op == '595f07bc': 41 | print('burn') 42 | slice.skip_bits(64) 43 | amount = slice.read_coins() 44 | print(amount/10**9) 45 | if op == '0f8a7ea5': 46 | print('transfer') 47 | slice.skip_bits(64) 48 | amount = slice.read_coins() 49 | print(amount / 10 ** 9) 50 | print(slice.read_msg_addr().to_string(True, True, True)) 51 | 52 | # print(cell) 53 | 54 | await client.close() 55 | 56 | 57 | if __name__ == '__main__': 58 | asyncio.run(pytonlib()) 59 | -------------------------------------------------------------------------------- /lesson_3/wallets.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import asyncio 3 | import requests 4 | 5 | 6 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 7 | from tonsdk.crypto import mnemonic_new 8 | from tonsdk.utils import to_nano 9 | 10 | from mnemoincs import mnemonics as my_mnemonics 11 | from pytonlib import TonlibClient 12 | 13 | 14 | def create_wallet(): 15 | mnemonics, pub_k, priv_k, wallet = Wallets.create(WalletVersionEnum.v3r2, workchain=0) 16 | 17 | return wallet 18 | 19 | 20 | def import_wallet(mnemoincs): 21 | mnemonics, pub_k, priv_k, wallet = Wallets.from_mnemonics(mnemonics=mnemoincs) 22 | 23 | return wallet 24 | 25 | 26 | async def get_client(): 27 | ton_config = requests.get('https://ton.org/global.config.json').json() 28 | 29 | keystore_dir = '/tmp/ton_keystore' 30 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 31 | 32 | # init TonlibClient 33 | client = TonlibClient(ls_index=2, # choose LiteServer index to connect 34 | config=ton_config, 35 | keystore=keystore_dir) 36 | 37 | # init tonlibjson 38 | await client.init() 39 | 40 | return client 41 | 42 | 43 | async def deploy_wallet(): 44 | wallet = import_wallet(my_mnemonics) 45 | 46 | boc = wallet.create_init_external_message()['message'].to_boc(False) 47 | 48 | client = await get_client() 49 | 50 | await client.raw_send_message(boc) 51 | 52 | await client.close() 53 | 54 | 55 | async def deploy_wallet_internal(): 56 | 57 | new_wallet = create_wallet() 58 | 59 | state_init = new_wallet.create_state_init()['state_init'] 60 | 61 | wallet = import_wallet(my_mnemonics) 62 | 63 | boc = wallet.create_transfer_message( 64 | to_addr=new_wallet.address.to_string(), 65 | amount=to_nano(0.05, 'ton'), 66 | seqno=1, 67 | state_init=state_init 68 | )['message'].to_boc(False) 69 | 70 | client = await get_client() 71 | 72 | await client.raw_send_message(boc) 73 | 74 | await client.close() 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | asyncio.run(deploy_wallet_internal()) 80 | -------------------------------------------------------------------------------- /lesson_4/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 4 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=mBDSZnqpDbo) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | * [tontools](https://github.com/yungwine/TonTools) 10 | * [TEPs](https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md) 11 | 12 | ### Official Docs - [link](https://docs.ton.org) 13 | 14 | ## Lesson themes 15 | - NFT collection creation and deployment 16 | - NFT (batch) minting 17 | - NFT transfer 18 | 19 | ## Folder structure 20 | - `client.py` - initializing of TonLibClient 21 | - `mint_bodies.py` - deployment bodies creation 22 | - `mint.py` - NFT minting 23 | - `transfer.py` - NFT transfer 24 | - `wallets.py`, `mnemoincs.py` - wallet import 25 | -------------------------------------------------------------------------------- /lesson_4/client.py: -------------------------------------------------------------------------------- 1 | import bitarray 2 | from pytonlib import TonlibClient 3 | from tonsdk.boc import Cell 4 | 5 | import requests 6 | from pathlib import Path 7 | 8 | from tvm_valuetypes import deserialize_boc, parse_hashmap 9 | 10 | 11 | async def get_client(ls_index: int, testnet: bool) -> TonlibClient: 12 | if testnet: 13 | url = 'https://ton.org/testnet-global.config.json' 14 | else: 15 | url = 'https://ton.org/global-config.json' 16 | 17 | config = requests.get(url).json() 18 | 19 | keystore_dir = '/tmp/ton_keystore' 20 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 21 | 22 | client = TonlibClient(ls_index=ls_index, config=config, keystore=keystore_dir, tonlib_timeout=10) 23 | await client.init() 24 | 25 | return client 26 | 27 | 28 | async def get_seqno(client: TonlibClient, address: str) -> int: 29 | data = await client.raw_run_method(method='seqno', stack_data=[], address=address) 30 | return int(data['stack'][0][1], 16) 31 | 32 | 33 | async def run_get_method(client: TonlibClient, address: str, method: str, stack: list) -> list: 34 | response = await client.raw_run_method(method=method, address=address, 35 | stack_data=stack) 36 | 37 | if response['exit_code'] != 0: 38 | raise Exception(f'get method exit code is {response["exit_code"]}') 39 | 40 | stack = response['stack'] 41 | 42 | return stack 43 | 44 | 45 | def read_dict(dict_cell: Cell, key_len: int) -> dict: 46 | cell = deserialize_boc(dict_cell.to_boc()) 47 | 48 | result = {} 49 | 50 | parse_hashmap(cell, key_len, result, bitarray.bitarray('')) 51 | 52 | for i in result: 53 | result[i] = Cell.one_from_boc(result[i].serialize_boc()) 54 | 55 | return result 56 | -------------------------------------------------------------------------------- /lesson_4/mint.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from client import * 4 | from mint_bodies import * 5 | from wallets import import_wallet 6 | from mnemoincs import mnemonics 7 | 8 | 9 | async def mint_collection(): 10 | client = await get_client(0, False) 11 | 12 | collection = create_collection() 13 | 14 | wallet = import_wallet(mnemonics) 15 | 16 | state_init = collection.create_state_init()['state_init'] 17 | collection_address = collection.address.to_string(True, True, True) 18 | 19 | query = wallet.create_transfer_message( 20 | to_addr=collection_address, 21 | state_init=state_init, 22 | seqno=await get_seqno(client, wallet.address.to_string()), 23 | amount=0.03 * 10**9 24 | ) 25 | 26 | await client.raw_send_message(query['message'].to_boc(False)) 27 | 28 | await client.close() 29 | 30 | 31 | async def mint_nft(): 32 | index = 0 33 | client = await get_client(0, False) 34 | 35 | collection = create_collection() 36 | 37 | wallet = import_wallet(mnemonics) 38 | 39 | body = create_mint_nft_body(index) 40 | 41 | collection_address = collection.address.to_string(True, True, True) 42 | 43 | query = wallet.create_transfer_message( 44 | to_addr=collection_address, 45 | seqno=await get_seqno(client, wallet.address.to_string()), 46 | payload=body, 47 | amount=0.03 * 10 ** 9 48 | ) 49 | await client.raw_send_message(query['message'].to_boc(False)) 50 | await client.close() 51 | 52 | 53 | async def mint_batch_nfts(): 54 | from_index = 5 55 | client = await get_client(0, False) 56 | 57 | collection = create_collection() 58 | 59 | wallet = import_wallet(mnemonics) 60 | 61 | items_amount = 2 62 | addresses = [Address('EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N'), Address('EQAhE3sLxHZpsyZ_HecMuwzvXHKLjYx4kEUehhOy2JmCcHCT')] 63 | contents = [f'{i + 1}/meta.json' for i in range(from_index, from_index + items_amount)] 64 | 65 | body = create_batch_nfts_mint_body(from_index, addresses, contents) 66 | 67 | collection_address = collection.address.to_string(True, True, True) 68 | 69 | query = wallet.create_transfer_message( 70 | to_addr=collection_address, 71 | seqno=await get_seqno(client, wallet.address.to_string()), 72 | payload=body, 73 | amount=0.06 * 10 ** 9 74 | ) 75 | await client.raw_send_message(query['message'].to_boc(False)) 76 | await client.close() 77 | 78 | 79 | if __name__ == '__main__': 80 | asyncio.run(mint_batch_nfts()) 81 | -------------------------------------------------------------------------------- /lesson_4/mint_bodies.py: -------------------------------------------------------------------------------- 1 | from tonsdk.contract.token.nft import NFTItem, NFTCollection 2 | from tonsdk.utils import Address 3 | 4 | 5 | def create_collection(): 6 | address = Address('EQB6DGcH-A8zy6U01dqPvKaeeMcnHZO-ztcditMHmNhddmMI') 7 | collection = NFTCollection( 8 | royalty=0.055, 9 | royalty_address=address, 10 | owner_address=address, 11 | nft_item_code_hex=NFTItem.code, 12 | collection_content_uri='https://s.getgems.io/nft/b/c/62fba50217c3fe3cbaad9e7f/meta.json', 13 | nft_item_content_base_uri='https://s.getgems.io/nft/b/c/62fba50217c3fe3cbaad9e7f/' 14 | ) 15 | return collection 16 | 17 | 18 | def create_mint_nft_body(index): 19 | collection = create_collection() 20 | 21 | body = collection.create_mint_body( 22 | index, 23 | Address('EQB6DGcH-A8zy6U01dqPvKaeeMcnHZO-ztcditMHmNhddmMI'), 24 | '1/meta.json', 25 | amount=0.02 * 10**9 26 | ) 27 | return body 28 | 29 | 30 | def create_batch_nfts_mint_body(from_index: int, addresses:list, contents: list): 31 | collection = create_collection() 32 | contents_and_owners = [] 33 | for i in range(len(addresses)): 34 | contents_and_owners.append((contents[i], addresses[i])) 35 | 36 | body = collection.create_batch_mint_body( 37 | from_item_index=from_index, 38 | contents_and_owners=contents_and_owners, 39 | amount_per_one=0.01 * 10 ** 9 40 | ) 41 | 42 | return body 43 | -------------------------------------------------------------------------------- /lesson_4/mnemoincs.py: -------------------------------------------------------------------------------- 1 | mnemonics = ['early', 'claw', 'echo', 'energy', 'erase', 'damp', 'expire', 'brush', 'scrub', 'ripple', 'skirt', 'beach', 'club', 'believe', 'firm', 'rely', 'head', 'neck', 'doll', 'punch', 'domain', 'phone', 'render', 'dad'] 2 | 3 | if __name__ == '__main__': 4 | pass -------------------------------------------------------------------------------- /lesson_4/transfers.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from TonTools import TonCenterClient, Wallet 4 | from secret import api_key 5 | 6 | from client import * 7 | from wallets import import_wallet 8 | from mnemoincs import mnemonics 9 | from tonsdk.contract.token.nft import NFTItem 10 | from tonsdk.utils import Address 11 | 12 | 13 | async def tontools(): 14 | client = TonCenterClient(api_key) 15 | 16 | wallet = Wallet(provider=client, mnemonics=mnemonics, version='v3r2') 17 | 18 | await wallet.transfer_nft( 19 | destination_address='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', 20 | nft_address='EQDL3cMdF72OKdKStvDXxpz2nsR1Y7tFx9sY5XrcwpEgBf3e', 21 | fee=0.06 22 | ) 23 | 24 | 25 | async def pytonlib(): 26 | client = await get_client(0, False) 27 | 28 | wallet = import_wallet(mnemonics) 29 | 30 | body = NFTItem().create_transfer_body( 31 | new_owner_address=Address('EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N'), 32 | ) 33 | 34 | query = wallet.create_transfer_message( 35 | to_addr='EQBafcMuCbIGwXkto1aWzVeQjzZVMtU_olVfQK7wElrwxBXr', 36 | seqno=await get_seqno(client, wallet.address.to_string()), 37 | payload=body, 38 | amount=0.06*10**9 39 | ) 40 | 41 | await client.raw_send_message(query['message'].to_boc(False)) 42 | 43 | await client.close() 44 | 45 | 46 | if __name__ == '__main__': 47 | asyncio.run(pytonlib()) 48 | -------------------------------------------------------------------------------- /lesson_4/wallets.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import asyncio 3 | import requests 4 | 5 | 6 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 7 | from tonsdk.crypto import mnemonic_new 8 | from tonsdk.utils import to_nano 9 | 10 | from mnemoincs import mnemonics as my_mnemonics 11 | from pytonlib import TonlibClient 12 | 13 | 14 | def create_wallet(): 15 | mnemonics, pub_k, priv_k, wallet = Wallets.create(WalletVersionEnum.v3r2, workchain=0) 16 | 17 | return wallet 18 | 19 | 20 | def import_wallet(mnemoincs): 21 | mnemonics, pub_k, priv_k, wallet = Wallets.from_mnemonics(mnemonics=mnemoincs) 22 | 23 | return wallet 24 | 25 | 26 | async def get_client(): 27 | ton_config = requests.get('https://ton.org/global.config.json').json() 28 | 29 | keystore_dir = '/tmp/ton_keystore' 30 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 31 | 32 | # init TonlibClient 33 | client = TonlibClient(ls_index=2, # choose LiteServer index to connect 34 | config=ton_config, 35 | keystore=keystore_dir) 36 | 37 | # init tonlibjson 38 | await client.init() 39 | 40 | return client 41 | 42 | 43 | async def deploy_wallet(): 44 | wallet = import_wallet(my_mnemonics) 45 | 46 | boc = wallet.create_init_external_message()['message'].to_boc(False) 47 | 48 | client = await get_client() 49 | 50 | await client.raw_send_message(boc) 51 | 52 | await client.close() 53 | 54 | 55 | async def deploy_wallet_internal(): 56 | 57 | new_wallet = create_wallet() 58 | 59 | state_init = new_wallet.create_state_init()['state_init'] 60 | 61 | wallet = import_wallet(my_mnemonics) 62 | 63 | boc = wallet.create_transfer_message( 64 | to_addr=new_wallet.address.to_string(), 65 | amount=to_nano(0.05, 'ton'), 66 | seqno=1, 67 | state_init=state_init 68 | )['message'].to_boc(False) 69 | 70 | client = await get_client() 71 | 72 | await client.raw_send_message(boc) 73 | 74 | await client.close() 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | asyncio.run(deploy_wallet_internal()) 80 | -------------------------------------------------------------------------------- /lesson_5/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 5 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=XPL97vlmfts) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | * [tontools](https://github.com/yungwine/TonTools) 10 | 11 | 12 | ### Official Docs - [link](https://docs.ton.org) 13 | 14 | ## Lesson themes 15 | - NFT collection parsing via get methods 16 | - Single NFT items parsing 17 | - NFT sale (auction) contracts parsing 18 | - Getting all NFT items by collection address 19 | 20 | ## Folder structure 21 | - `client.py` - useful funcs with TonLibClient 22 | - `get_items_by_collection.py` - getting all NFT items by collection address 23 | - `parse_collection.py` - NFT Collection parsing 24 | - `parse_nft_items.py` - NFT parsing 25 | - `parse_sale.py` - NFT Sales parsing 26 | -------------------------------------------------------------------------------- /lesson_5/client.py: -------------------------------------------------------------------------------- 1 | import bitarray 2 | from pytonlib import TonlibClient 3 | from tonsdk.boc import Cell 4 | 5 | import requests 6 | from pathlib import Path 7 | 8 | from tvm_valuetypes import deserialize_boc, parse_hashmap 9 | 10 | 11 | async def get_client(ls_index: int, testnet: bool) -> TonlibClient: 12 | if testnet: 13 | url = 'https://ton.org/testnet-global.config.json' 14 | else: 15 | url = 'https://ton.org/global-config.json' 16 | 17 | config = requests.get(url).json() 18 | 19 | keystore_dir = '/tmp/ton_keystore' 20 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 21 | 22 | client = TonlibClient(ls_index=ls_index, config=config, keystore=keystore_dir, tonlib_timeout=10) 23 | await client.init() 24 | 25 | return client 26 | 27 | 28 | async def get_seqno(client: TonlibClient, address: str) -> int: 29 | data = await client.raw_run_method(method='seqno', stack_data=[], address=address) 30 | return int(data['stack'][0][1], 16) 31 | 32 | 33 | async def run_get_method(client: TonlibClient, address: str, method: str, stack: list) -> list: 34 | response = await client.raw_run_method(method=method, address=address, 35 | stack_data=stack) 36 | 37 | if response['exit_code'] != 0: 38 | raise Exception(f'get method exit code is {response["exit_code"]}') 39 | 40 | stack = response['stack'] 41 | 42 | return stack 43 | 44 | 45 | def read_dict(dict_cell: Cell, key_len: int) -> dict: 46 | cell = deserialize_boc(dict_cell.to_boc()) 47 | 48 | result = {} 49 | 50 | parse_hashmap(cell, key_len, result, bitarray.bitarray('')) 51 | 52 | for i in result: 53 | result[i] = Cell.one_from_boc(result[i].serialize_boc()) 54 | 55 | return result 56 | -------------------------------------------------------------------------------- /lesson_5/get_items_by_collection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from parse_collection import get_nft_address_by_index, get_collection_data 4 | from client import * 5 | 6 | 7 | async def main(): 8 | collection_address = 'EQAG2BH0JlmFkbMrLEnyn2bIITaOSssd4WdisE4BdFMkZbir' 9 | client = await get_client(0, False) 10 | next_item_index, _, _ =await get_collection_data(client, collection_address) 11 | 12 | for i in range(0, next_item_index): 13 | address = await get_nft_address_by_index(client, collection_address, i) 14 | print(i, address) 15 | 16 | await client.close() 17 | 18 | 19 | if __name__ == '__main__': 20 | asyncio.run(main()) 21 | -------------------------------------------------------------------------------- /lesson_5/parse_collection.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from TonTools import TonCenterClient, NftCollection 4 | from secret import api_key 5 | from client import * 6 | 7 | import requests 8 | import base64 9 | 10 | 11 | async def tontools(): 12 | client = TonCenterClient(key=api_key) 13 | collection = NftCollection('EQAG2BH0JlmFkbMrLEnyn2bIITaOSssd4WdisE4BdFMkZbir', provider=client) 14 | await collection.update() 15 | print(collection) 16 | 17 | 18 | async def get_collection_data(client: TonlibClient, address='EQAG2BH0JlmFkbMrLEnyn2bIITaOSssd4WdisE4BdFMkZbir'): 19 | 20 | stack = await run_get_method(client, address=address, 21 | method='get_collection_data', stack=[]) 22 | next_item_index = int(stack[0][1], 16) 23 | content = Cell.one_from_boc(base64.b64decode(stack[1][1]['bytes'])).bits.get_top_upped_array().decode().split('\x01')[-1] 24 | metadata = requests.get(content).json() 25 | owner_address = Cell.one_from_boc(base64.b64decode(stack[2][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 26 | return next_item_index, metadata, owner_address 27 | 28 | 29 | async def get_nft_address_by_index(client: TonlibClient, collection_address: str = 'EQAG2BH0JlmFkbMrLEnyn2bIITaOSssd4WdisE4BdFMkZbir', index=7693): 30 | request_stack = [ 31 | ["num", index] 32 | ] 33 | 34 | stack = await run_get_method(client, address=collection_address, 35 | method='get_nft_address_by_index', stack=request_stack) 36 | 37 | nft_address = Cell.one_from_boc(base64.b64decode(stack[0][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 38 | 39 | return nft_address 40 | 41 | 42 | async def royalty_params(client: TonlibClient, address: str = 'EQAG2BH0JlmFkbMrLEnyn2bIITaOSssd4WdisE4BdFMkZbir'): 43 | stack = await run_get_method(client, address=address, method='royalty_params', stack=[]) 44 | 45 | royalty_factor = int(stack[0][1], 16) 46 | royalty_base = int(stack[1][1], 16) 47 | royalty_address = Cell.one_from_boc(base64.b64decode(stack[2][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 48 | 49 | return royalty_factor, royalty_base, royalty_address 50 | 51 | 52 | async def get_nft_content(client: TonlibClient, index: int, individual_content: str): 53 | request_stack = [ 54 | ["num", index], 55 | ["tvm.Cell", individual_content] 56 | ] 57 | 58 | stack = await run_get_method(client, address='EQAG2BH0JlmFkbMrLEnyn2bIITaOSssd4WdisE4BdFMkZbir', 59 | method='get_nft_content', stack=request_stack) 60 | 61 | content = Cell.one_from_boc(base64.b64decode(stack[0][1]['bytes'])) 62 | 63 | str = content.bits.get_top_upped_array() + content.refs[0].bits.get_top_upped_array() 64 | link = str.decode().split('\x01')[-1] 65 | return link 66 | 67 | 68 | async def pytonlib(): 69 | client = await get_client(0, False) 70 | 71 | # result = await get_collection_data(client) 72 | # result = await get_nft_address_by_index(client) 73 | # result = await royalty_params(client) 74 | result = await get_nft_content(client, 7693, 'te6cckEBAQEAEAAAHDc2OTMvNzY5My5qc29uLdxv9g==') 75 | 76 | print(result) 77 | 78 | await client.close() 79 | 80 | 81 | if __name__ == '__main__': 82 | asyncio.run(pytonlib()) 83 | -------------------------------------------------------------------------------- /lesson_5/parse_nft_items.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from TonTools import TonCenterClient, NftItem 4 | from secret import api_key 5 | from client import * 6 | 7 | from parse_collection import get_nft_content 8 | from parse_sale import get_sale_data 9 | 10 | import requests 11 | import base64 12 | 13 | 14 | async def tontools(): 15 | client = TonCenterClient(key=api_key) 16 | item = NftItem('EQCM8qfFcQVFGW-_6xGSovpQxBJUakja1LrJ24-vlQThJfUE', provider=client) 17 | await item.update() 18 | print(item) 19 | 20 | 21 | async def get_nft_data(client: TonlibClient, address): 22 | 23 | stack = await run_get_method(client, address=address, 24 | method='get_nft_data', stack=[]) 25 | 26 | index = int(stack[1][1], 16) 27 | 28 | content_link = await get_nft_content(client, index, stack[4][1]['bytes']) 29 | 30 | metadata = requests.get(content_link).json() 31 | 32 | collection_address = Cell.one_from_boc(base64.b64decode(stack[2][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 33 | 34 | owner_address = Cell.one_from_boc(base64.b64decode(stack[3][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 35 | 36 | result = await get_sale_data(client, owner_address) 37 | if result[0]: 38 | owner_address = result[1] 39 | 40 | return index, collection_address, owner_address, metadata 41 | 42 | 43 | async def pytonlib(): 44 | client = await get_client(0, False) 45 | 46 | result = await get_nft_data(client, 'EQAj_sI_Gb_IWdFzubAnKxRV7nL18V3KqvBnIf8eLIvH_2j4') 47 | 48 | print(result) 49 | 50 | await client.close() 51 | 52 | 53 | if __name__ == '__main__': 54 | asyncio.run(pytonlib()) 55 | -------------------------------------------------------------------------------- /lesson_5/parse_sale.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from TonTools import TonCenterClient, NftItem 4 | from secret import api_key 5 | from client import * 6 | 7 | from parse_collection import get_nft_content 8 | 9 | import requests 10 | import base64 11 | 12 | 13 | async def get_sale_data(client: TonlibClient, address='EQCD1lACPLbCaZ0oO9L3lKZOEq4Y1QeY8XSPv4nS658QEZoi'): 14 | try: 15 | stack = await run_get_method(client, address=address, 16 | method='get_sale_data', stack=[]) 17 | except: 18 | return (False, ) 19 | 20 | fix_magic = '0x46495850' 21 | auction_magic = '0x415543' 22 | if stack[0][1] == fix_magic: 23 | is_complete = bool(int(stack[1][1], 16)) 24 | created_at = int(stack[2][1], 16) 25 | marketplace_address = Cell.one_from_boc(base64.b64decode(stack[3][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 26 | nft_address = Cell.one_from_boc(base64.b64decode(stack[4][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 27 | nft_owner_address = Cell.one_from_boc(base64.b64decode(stack[5][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 28 | full_price = int(stack[6][1], 16) 29 | 30 | return True, nft_owner_address, full_price 31 | 32 | elif stack[0][1] == auction_magic: 33 | is_complete = bool(int(stack[1][1], 16)) 34 | end_at = int(stack[2][1], 16) 35 | marketplace_address = Cell.one_from_boc(base64.b64decode(stack[3][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 36 | nft_address = Cell.one_from_boc(base64.b64decode(stack[4][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 37 | nft_owner_address = Cell.one_from_boc(base64.b64decode(stack[5][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 38 | last_bid = int(stack[6][1], 16) 39 | min_bid = int(stack[16][1], 16) 40 | return True, nft_owner_address, max(last_bid, min_bid) 41 | 42 | 43 | async def pytonlib(): 44 | client = await get_client(0, False) 45 | 46 | result = await get_sale_data(client, 'EQCD1lACPLbCaZ0oO9L3lKZOEq4Y1QeY8XSPv4nS658QEZoi') 47 | 48 | print(result) 49 | 50 | await client.close() 51 | 52 | 53 | if __name__ == '__main__': 54 | asyncio.run(pytonlib()) 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /lesson_6/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 6 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=cT9M54Y3uc8) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | * [tontools](https://github.com/yungwine/TonTools) 10 | * 11 | * [TEPs](https://github.com/ton-blockchain/TEPs/blob/master/text/0064-token-data-standard.md) 12 | 13 | 14 | ### Official Docs - [link](https://docs.ton.org) 15 | 16 | ## Lesson themes 17 | - Jetton minter deployment 18 | - Jettons mint 19 | - Jettons transfer 20 | - Jettons burn 21 | 22 | ## Folder structure 23 | - `client.py` - useful funcs with TonLibClient 24 | - `encode_image.py` - encoding image to b64 str script 25 | - `image.webp` - initial image 26 | - `image.raw` - encoded image 27 | - `mint_bodies.py` - deployment bodies creation 28 | - `deploy.py` - deploying contracts 29 | - `transfer.py` - Jettons transfer 30 | - `burn.py` - Jettons burn 31 | - `wallets.py`, `mnemoincs.py` - wallet import 32 | 33 | -------------------------------------------------------------------------------- /lesson_6/burn.py: -------------------------------------------------------------------------------- 1 | from TonTools import TonCenterClient, Wallet 2 | import asyncio 3 | 4 | from secret import api_key 5 | from client import * 6 | from wallets import import_wallet 7 | from mnemoincs import mnemonics 8 | from mint_bodies import * 9 | 10 | 11 | async def burn(): 12 | client = await get_client(0, False) 13 | 14 | wallet = import_wallet(mnemonics) 15 | 16 | body = JettonWallet().create_burn_body( 17 | jetton_amount=100_000*10**9, 18 | ) 19 | 20 | query = wallet.create_transfer_message( 21 | to_addr='EQCOsF1sD90GgKmy0wAlxMkj1CTDp4WKrPuFJxF1adTIPvEG', 22 | seqno= await get_seqno(client, wallet.address.to_string()), 23 | payload=body, 24 | amount=0.03*10**9 25 | ) 26 | 27 | await client.raw_send_message(query['message'].to_boc(False)) 28 | 29 | await client.close() 30 | 31 | 32 | async def revoke_rights(): 33 | client = await get_client(0, False) 34 | 35 | wallet = import_wallet(mnemonics) 36 | 37 | body = JettonMinter().create_change_admin_body( 38 | new_admin_address=Address('EQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c'), 39 | ) 40 | 41 | query = wallet.create_transfer_message( 42 | to_addr='EQDY5L5YuNUp-Tohu-Fqetpz0R26v8e7H0FXRjp2whL4QLWh', 43 | seqno=await get_seqno(client, wallet.address.to_string()), 44 | payload=body, 45 | amount=0.03 * 10 ** 9 46 | ) 47 | 48 | await client.raw_send_message(query['message'].to_boc(False)) 49 | 50 | await client.close() 51 | 52 | 53 | if __name__ == '__main__': 54 | asyncio.run(revoke_rights()) 55 | -------------------------------------------------------------------------------- /lesson_6/client.py: -------------------------------------------------------------------------------- 1 | import bitarray 2 | from pytonlib import TonlibClient 3 | from tonsdk.boc import Cell 4 | 5 | import requests 6 | from pathlib import Path 7 | 8 | from tvm_valuetypes import deserialize_boc, parse_hashmap 9 | 10 | 11 | async def get_client(ls_index: int, testnet: bool) -> TonlibClient: 12 | if testnet: 13 | url = 'https://ton.org/testnet-global.config.json' 14 | else: 15 | url = 'https://ton.org/global-config.json' 16 | 17 | config = requests.get(url).json() 18 | 19 | keystore_dir = '/tmp/ton_keystore' 20 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 21 | 22 | client = TonlibClient(ls_index=ls_index, config=config, keystore=keystore_dir, tonlib_timeout=10) 23 | await client.init() 24 | 25 | return client 26 | 27 | 28 | async def get_seqno(client: TonlibClient, address: str) -> int: 29 | data = await client.raw_run_method(method='seqno', stack_data=[], address=address) 30 | return int(data['stack'][0][1], 16) 31 | 32 | 33 | async def run_get_method(client: TonlibClient, address: str, method: str, stack: list) -> list: 34 | response = await client.raw_run_method(method=method, address=address, 35 | stack_data=stack) 36 | 37 | if response['exit_code'] != 0: 38 | raise Exception(f'get method exit code is {response["exit_code"]}') 39 | 40 | stack = response['stack'] 41 | 42 | return stack 43 | 44 | 45 | def read_dict(dict_cell: Cell, key_len: int) -> dict: 46 | cell = deserialize_boc(dict_cell.to_boc()) 47 | 48 | result = {} 49 | 50 | parse_hashmap(cell, key_len, result, bitarray.bitarray('')) 51 | 52 | for i in result: 53 | result[i] = Cell.one_from_boc(result[i].serialize_boc()) 54 | 55 | return result 56 | -------------------------------------------------------------------------------- /lesson_6/deploy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from client import * 4 | from wallets import import_wallet 5 | from mnemoincs import mnemonics 6 | from mint_bodies import * 7 | 8 | 9 | async def deploy_minter(): 10 | client = await get_client(0, False) 11 | 12 | minter = create_minter() 13 | 14 | wallet = import_wallet(mnemonics) 15 | 16 | state_init = minter.create_state_init()['state_init'] 17 | minter_address = minter.address.to_string(True, True, True) 18 | 19 | query = wallet.create_transfer_message( 20 | to_addr=minter_address, 21 | state_init=state_init, 22 | seqno=await get_seqno(client, wallet.address.to_string()), 23 | amount=0.03 * 10 ** 9 24 | ) 25 | 26 | await client.raw_send_message(query['message'].to_boc(False)) 27 | 28 | await client.close() 29 | 30 | 31 | async def mint_tokens(): 32 | client = await get_client(0, False) 33 | 34 | minter = create_minter() 35 | 36 | wallet = import_wallet(mnemonics) 37 | 38 | body = create_mint_tokens_body() 39 | minter_address = minter.address.to_string(True, True, True) 40 | 41 | query = wallet.create_transfer_message( 42 | to_addr=minter_address, 43 | payload=body, 44 | seqno=await get_seqno(client, wallet.address.to_string()), 45 | amount=0.05 * 10 ** 9 46 | ) 47 | 48 | await client.raw_send_message(query['message'].to_boc(False)) 49 | 50 | await client.close() 51 | 52 | 53 | if __name__ == '__main__': 54 | asyncio.run(mint_tokens()) 55 | -------------------------------------------------------------------------------- /lesson_6/encode_image.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | 4 | with open('image.webp', 'rb') as f: 5 | img = open('image.raw', 'wb') 6 | img.write(base64.b64encode(f.read())) 7 | img.close() 8 | 9 | if __name__ == '__main__': 10 | pass -------------------------------------------------------------------------------- /lesson_6/image.raw: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /lesson_6/image.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TonDevStudy/pyton-lessons-eng/69360457eb2441837ab956ed3073a3ceab7ba02a/lesson_6/image.webp -------------------------------------------------------------------------------- /lesson_6/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Test token", 3 | "description": "Pyton lessons https://github.com/TonDevStudy/pyton-lessons-eng", 4 | "symbol": "TST", 5 | "decimals": 9, 6 | "image_data": "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" 7 | } -------------------------------------------------------------------------------- /lesson_6/mint_bodies.py: -------------------------------------------------------------------------------- 1 | from tonsdk.contract.token.ft import JettonMinter, JettonWallet 2 | from tonsdk.utils import Address 3 | 4 | 5 | def create_minter(): 6 | minter = JettonMinter( 7 | admin_address=Address('EQB6DGcH-A8zy6U01dqPvKaeeMcnHZO-ztcditMHmNhddmMI'), 8 | jetton_content_uri='https://raw.githubusercontent.com/TonDevStudy/pyton-lessons-eng/main/lesson_6/metadata.json', 9 | jetton_wallet_code_hex=JettonWallet.code, 10 | ) 11 | 12 | return minter 13 | 14 | 15 | def create_mint_tokens_body(): 16 | minter = create_minter() 17 | 18 | body = minter.create_mint_body( 19 | Address('EQB6DGcH-A8zy6U01dqPvKaeeMcnHZO-ztcditMHmNhddmMI'), 20 | jetton_amount=1_000_000 * 10**9, amount=0.02*10**9 21 | ) 22 | 23 | return body 24 | -------------------------------------------------------------------------------- /lesson_6/mnemoincs.py: -------------------------------------------------------------------------------- 1 | mnemonics = ['early', 'claw', 'echo', 'energy', 'erase', 'damp', 'expire', 'brush', 'scrub', 'ripple', 'skirt', 'beach', 'club', 'believe', 'firm', 'rely', 'head', 'neck', 'doll', 'punch', 'domain', 'phone', 'render', 'dad'] 2 | 3 | if __name__ == '__main__': 4 | pass -------------------------------------------------------------------------------- /lesson_6/transfer.py: -------------------------------------------------------------------------------- 1 | from TonTools import TonCenterClient, Wallet 2 | import asyncio 3 | 4 | from secret import api_key 5 | from client import * 6 | from wallets import import_wallet 7 | from mnemoincs import mnemonics 8 | from mint_bodies import * 9 | 10 | 11 | async def tontools(): 12 | client = TonCenterClient(key=api_key) 13 | wallet = Wallet(mnemonics=mnemonics, version='v3r2', provider=client) 14 | 15 | resp = await wallet.transfer_jetton_by_jetton_wallet(destination_address='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', 16 | jettons_amount=1000, jetton_wallet='EQCOsF1sD90GgKmy0wAlxMkj1CTDp4WKrPuFJxF1adTIPvEG') 17 | 18 | resp = await wallet.transfer_jetton( 19 | destination_address='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', 20 | jettons_amount=1000, jetton_master_address='EQDY5L5YuNUp-Tohu-Fqetpz0R26v8e7H0FXRjp2whL4QLWh') 21 | print(resp) 22 | 23 | 24 | async def pytonlib(): 25 | client = await get_client(0, False) 26 | 27 | wallet = import_wallet(mnemonics) 28 | 29 | body = JettonWallet().create_transfer_body( 30 | to_address=Address('EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N'), 31 | jetton_amount=1000*10**9, 32 | ) 33 | 34 | query = wallet.create_transfer_message( 35 | to_addr='EQCOsF1sD90GgKmy0wAlxMkj1CTDp4WKrPuFJxF1adTIPvEG', 36 | seqno= await get_seqno(client, wallet.address.to_string()), 37 | payload=body, 38 | amount=0.06*10**9 39 | ) 40 | 41 | await client.raw_send_message(query['message'].to_boc(False)) 42 | 43 | await client.close() 44 | 45 | 46 | if __name__ == '__main__': 47 | asyncio.run(pytonlib()) 48 | 49 | -------------------------------------------------------------------------------- /lesson_6/wallets.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import asyncio 3 | import requests 4 | 5 | 6 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 7 | from tonsdk.crypto import mnemonic_new 8 | from tonsdk.utils import to_nano 9 | 10 | from mnemoincs import mnemonics as my_mnemonics 11 | from pytonlib import TonlibClient 12 | 13 | 14 | def create_wallet(): 15 | mnemonics, pub_k, priv_k, wallet = Wallets.create(WalletVersionEnum.v3r2, workchain=0) 16 | 17 | return wallet 18 | 19 | 20 | def import_wallet(mnemoincs): 21 | mnemonics, pub_k, priv_k, wallet = Wallets.from_mnemonics(mnemonics=mnemoincs) 22 | 23 | return wallet 24 | 25 | 26 | async def get_client(): 27 | ton_config = requests.get('https://ton.org/global.config.json').json() 28 | 29 | keystore_dir = '/tmp/ton_keystore' 30 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 31 | 32 | # init TonlibClient 33 | client = TonlibClient(ls_index=2, # choose LiteServer index to connect 34 | config=ton_config, 35 | keystore=keystore_dir) 36 | 37 | # init tonlibjson 38 | await client.init() 39 | 40 | return client 41 | 42 | 43 | async def deploy_wallet(): 44 | wallet = import_wallet(my_mnemonics) 45 | 46 | boc = wallet.create_init_external_message()['message'].to_boc(False) 47 | 48 | client = await get_client() 49 | 50 | await client.raw_send_message(boc) 51 | 52 | await client.close() 53 | 54 | 55 | async def deploy_wallet_internal(): 56 | 57 | new_wallet = create_wallet() 58 | 59 | state_init = new_wallet.create_state_init()['state_init'] 60 | 61 | wallet = import_wallet(my_mnemonics) 62 | 63 | boc = wallet.create_transfer_message( 64 | to_addr=new_wallet.address.to_string(), 65 | amount=to_nano(0.05, 'ton'), 66 | seqno=1, 67 | state_init=state_init 68 | )['message'].to_boc(False) 69 | 70 | client = await get_client() 71 | 72 | await client.raw_send_message(boc) 73 | 74 | await client.close() 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | asyncio.run(deploy_wallet_internal()) 80 | -------------------------------------------------------------------------------- /lesson_7/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 7 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=7WhF15NA5P8) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | * [tontools](https://github.com/yungwine/TonTools) 10 | 11 | 12 | ### Official Docs - [link](https://docs.ton.org) 13 | 14 | ## Lesson themes 15 | - Jetton content parsing 16 | - Jetton onchain and offchain metadata parsing 17 | - HashMaps introduction 18 | - Jetton wallet parsing 19 | 20 | ## Folder structure 21 | - `client.py` - useful funcs with TonLibClient 22 | - `jetton_minter_parsing.py` - parsing of Jetton Minter contract 23 | - `jetton_wallet_parsing.py` - parsing of Jetton wallet contract 24 | -------------------------------------------------------------------------------- /lesson_7/client.py: -------------------------------------------------------------------------------- 1 | import bitarray 2 | from pytonlib import TonlibClient 3 | from tonsdk.boc import Cell 4 | 5 | import requests 6 | from pathlib import Path 7 | 8 | from tvm_valuetypes import deserialize_boc, parse_hashmap 9 | 10 | 11 | async def get_client(ls_index: int, testnet: bool) -> TonlibClient: 12 | if testnet: 13 | url = 'https://ton.org/testnet-global.config.json' 14 | else: 15 | url = 'https://ton.org/global-config.json' 16 | 17 | config = requests.get(url).json() 18 | 19 | keystore_dir = '/tmp/ton_keystore' 20 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 21 | 22 | client = TonlibClient(ls_index=ls_index, config=config, keystore=keystore_dir, tonlib_timeout=10) 23 | await client.init() 24 | 25 | return client 26 | 27 | 28 | async def get_seqno(client: TonlibClient, address: str) -> int: 29 | data = await client.raw_run_method(method='seqno', stack_data=[], address=address) 30 | return int(data['stack'][0][1], 16) 31 | 32 | 33 | async def run_get_method(client: TonlibClient, address: str, method: str, stack: list) -> list: 34 | response = await client.raw_run_method(method=method, address=address, 35 | stack_data=stack) 36 | 37 | if response['exit_code'] != 0: 38 | raise Exception(f'get method exit code is {response["exit_code"]}') 39 | 40 | stack = response['stack'] 41 | 42 | return stack 43 | 44 | 45 | def read_dict(dict_cell: Cell, key_len: int) -> dict: 46 | cell = deserialize_boc(dict_cell.to_boc()) 47 | 48 | result = {} 49 | 50 | parse_hashmap(cell, key_len, result, bitarray.bitarray('')) 51 | 52 | for i in result: 53 | result[i] = Cell.one_from_boc(result[i].serialize_boc()) 54 | 55 | return result 56 | -------------------------------------------------------------------------------- /lesson_7/jetton_minter_parsing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | import hashlib 4 | import bitarray 5 | 6 | import requests 7 | from TonTools import TonCenterClient, Jetton 8 | from secret import api_key 9 | from client import * 10 | from tonsdk.boc import begin_cell 11 | from tonsdk.utils import Address 12 | 13 | async def tontools(): 14 | client = TonCenterClient(key=api_key) 15 | minter = Jetton('EQDCJL0iQHofcBBvFBHdVG233Ri2V4kCNFgfRT-gqAd3Oc86', provider=client) 16 | await minter.update() 17 | print(minter) 18 | 19 | 20 | def parse_offchain(stack): 21 | content = \ 22 | Cell.one_from_boc(base64.b64decode(stack[3][1]['bytes'])).bits.get_top_upped_array().decode().split('\x01')[-1] 23 | 24 | metadata = requests.get('https://ipfs.io/ipfs/' + content.split('ipfs://')[1]).json() 25 | return metadata 26 | 27 | 28 | def parse_onchain(cell: Cell): 29 | parsed_dict = read_dict(cell.begin_parse().read_ref(), 256) 30 | result = {} 31 | for i in ['name', 'description', 'image', 'symbol']: 32 | array = bitarray.bitarray() 33 | array.frombytes(hashlib.sha256(i.encode()).digest()) 34 | array.to01() 35 | value = parsed_dict.get(array.to01(), None) 36 | if value is not None: 37 | result[i] = value.refs[0].bits.get_top_upped_array().decode().split('\x00')[-1] 38 | 39 | return result 40 | 41 | 42 | async def pytonlib(): 43 | client = await get_client(0, False) 44 | 45 | stack = await run_get_method(client, method='get_jetton_data', address='EQDCJL0iQHofcBBvFBHdVG233Ri2V4kCNFgfRT-gqAd3Oc86', stack=[]) 46 | 47 | total_supply = int(stack[0][1], 16) 48 | 49 | owner_address = Cell.one_from_boc(base64.b64decode(stack[2][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 50 | 51 | # content = Cell.one_from_boc(base64.b64decode(stack[3][1]['bytes'])).bits.get_top_upped_array().decode().split('\x01')[-1] 52 | 53 | # metadata = parse_offchain(stack) 54 | metadata = parse_onchain(Cell.one_from_boc(base64.b64decode(stack[3][1]['bytes']))) 55 | 56 | print(total_supply, owner_address, metadata) 57 | 58 | await client.close() 59 | 60 | 61 | async def get_wallet_address(): 62 | jetton_minter = 'EQDCJL0iQHofcBBvFBHdVG233Ri2V4kCNFgfRT-gqAd3Oc86' 63 | owner_wallet = 'EQBvW8Z5huBkMJYdnfAEM5JqTNkuWX3diqYENkWsIL0XggGG' 64 | client = await get_client(0, False) 65 | 66 | request_stack = [ 67 | ["tvm.Slice", 68 | base64.b64encode(begin_cell().store_address(Address(owner_wallet)).end_cell().to_boc()).decode()] 69 | ] 70 | 71 | stack = await run_get_method(client, method='get_wallet_address', address=jetton_minter, stack=request_stack) 72 | jetton_wallet = Cell.one_from_boc(base64.b64decode(stack[0][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 73 | print(jetton_wallet) 74 | await client.close() 75 | 76 | 77 | if __name__ == '__main__': 78 | asyncio.run(get_wallet_address()) 79 | -------------------------------------------------------------------------------- /lesson_7/jetton_wallet_parsing.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | import hashlib 4 | import bitarray 5 | 6 | import requests 7 | from TonTools import TonCenterClient, Jetton, JettonWallet 8 | 9 | 10 | from secret import api_key 11 | from client import * 12 | from tonsdk.boc import begin_cell 13 | from tonsdk.utils import Address 14 | 15 | 16 | async def tontools(): 17 | client = TonCenterClient(key=api_key) 18 | wallet = JettonWallet('EQC9QpdKuL0s5kNYdq-ELm-iFw4PUFX-l-FpHLQ12O_AbaPe', provider=client) 19 | await wallet.update() 20 | print(wallet) 21 | 22 | 23 | async def pytonlib(): 24 | client = await get_client(0, False) 25 | 26 | stack = await run_get_method(client, method='get_wallet_data', address='EQC9QpdKuL0s5kNYdq-ELm-iFw4PUFX-l-FpHLQ12O_AbaPe', stack=[]) 27 | 28 | balance = int(stack[0][1], 16) 29 | 30 | owner_address = Cell.one_from_boc(base64.b64decode(stack[1][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 31 | jetton_master_address = Cell.one_from_boc(base64.b64decode(stack[2][1]['bytes'])).begin_parse().read_msg_addr().to_string(True, True, True) 32 | 33 | print(balance, owner_address, jetton_master_address) 34 | 35 | await client.close() 36 | 37 | 38 | if __name__ == '__main__': 39 | asyncio.run(pytonlib()) 40 | -------------------------------------------------------------------------------- /lesson_8/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 8 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://youtu.be/GLGGk_akkJk) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | 10 | 11 | * [Config params description](https://docs.ton.org/develop/howto/blockchain-configs) 12 | * [Raw config params values](https://explorer.toncoin.org/config) 13 | 14 | ### Official Docs - [link](https://docs.ton.org) 15 | 16 | ## Lesson themes 17 | - Highload wallets: deployment, transfer 18 | - HashMaps reading 19 | - Blockchain config parsing 20 | 21 | ## Folder structure 22 | - `client.py` - useful funcs with TonLibClient 23 | - `deploy.py` - Highload wallet deployment 24 | - `wallets.py`, `mnemoincs.py` - wallet import 25 | - `transfer.py` - transfer using Highload wallet 26 | - `get_processed.py` - check if query_id was processed 27 | - `parse_hashmap.py` - hashmap reading 28 | - `config.py` - blockchain configuration parsing 29 | -------------------------------------------------------------------------------- /lesson_8/client.py: -------------------------------------------------------------------------------- 1 | import bitarray 2 | from pytonlib import TonlibClient 3 | from tonsdk.boc import Cell 4 | 5 | import requests 6 | from pathlib import Path 7 | 8 | from tvm_valuetypes import deserialize_boc, parse_hashmap 9 | 10 | 11 | async def get_client(ls_index: int, testnet: bool) -> TonlibClient: 12 | if testnet: 13 | url = 'https://ton.org/testnet-global.config.json' 14 | else: 15 | url = 'https://ton.org/global-config.json' 16 | 17 | config = requests.get(url).json() 18 | 19 | keystore_dir = '/tmp/ton_keystore' 20 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 21 | 22 | client = TonlibClient(ls_index=ls_index, config=config, keystore=keystore_dir, tonlib_timeout=10) 23 | await client.init() 24 | 25 | return client 26 | 27 | 28 | async def get_seqno(client: TonlibClient, address: str) -> int: 29 | data = await client.raw_run_method(method='seqno', stack_data=[], address=address) 30 | return int(data['stack'][0][1], 16) 31 | 32 | 33 | async def run_get_method(client: TonlibClient, address: str, method: str, stack: list) -> list: 34 | response = await client.raw_run_method(method=method, address=address, 35 | stack_data=stack) 36 | 37 | if response['exit_code'] != 0: 38 | raise Exception(f'get method exit code is {response["exit_code"]}') 39 | 40 | stack = response['stack'] 41 | 42 | return stack 43 | 44 | 45 | def read_dict(dict_cell: Cell, key_len: int) -> dict: 46 | cell = deserialize_boc(dict_cell.to_boc()) 47 | 48 | result = {} 49 | 50 | parse_hashmap(cell, key_len, result, bitarray.bitarray('')) 51 | 52 | for i in result: 53 | result[i] = Cell.one_from_boc(result[i].serialize_boc()) 54 | 55 | return result 56 | -------------------------------------------------------------------------------- /lesson_8/config.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from wallets import import_wallet 4 | from mnemoincs import mnemonics 5 | from client import * 6 | 7 | import base64 8 | 9 | from tonsdk.boc import Cell, Slice 10 | 11 | 12 | def parse_16(slice: Slice): 13 | max_validators = slice.read_uint(16) 14 | max_main_validators = slice.read_uint(16) 15 | min_validators = slice.read_uint(16) 16 | 17 | return { 18 | 'max_validators': max_validators, 19 | 'max_main_validators': max_main_validators, 20 | 'min_validators': min_validators 21 | } 22 | 23 | 24 | def parse_17(slice: Slice): 25 | min_stake = slice.read_coins() 26 | max_stake = slice.read_coins() 27 | min_total_stake = slice.read_coins() 28 | max_stake_factor = slice.read_coins() 29 | 30 | return { 31 | 'min_stake': min_stake/10**9, 32 | 'max_stake': max_stake/10**9, 33 | 'min_total_stake': min_total_stake/10**9, 34 | 'max_stake_factor': max_stake_factor, 35 | } 36 | 37 | 38 | def parse_34(slice: Slice): 39 | slice.skip_bits(8) 40 | utime_since = slice.read_uint(32) 41 | utime_until = slice.read_uint(32) 42 | total = slice.read_uint(16) 43 | main = slice.read_uint(16) 44 | 45 | return { 46 | 'utime_since': utime_since, 47 | 'utime_until': utime_until, 48 | 'total': total, 49 | 'main': main, 50 | } 51 | 52 | 53 | async def main(): 54 | client = await get_client(7, False) 55 | 56 | seqno = (await client.get_masterchain_info())['last']['seqno'] 57 | 58 | param16 = Cell.one_from_boc(base64.b64decode((await client.get_config_param(16, seqno))['config']['bytes'])).begin_parse() 59 | 60 | param17 = Cell.one_from_boc(base64.b64decode((await client.get_config_param(17, seqno))['config']['bytes'])).begin_parse() 61 | 62 | param34 = Cell.one_from_boc(base64.b64decode((await client.get_config_param(34, seqno))['config']['bytes'])).begin_parse() 63 | 64 | 65 | print(parse_16(param16)) 66 | print(parse_17(param17)) 67 | print(parse_34(param34)) 68 | 69 | 70 | await client.close() 71 | 72 | 73 | if __name__ == '__main__': 74 | asyncio.run(main()) 75 | -------------------------------------------------------------------------------- /lesson_8/deploy.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from wallets import import_wallet 4 | from mnemoincs import mnemonics 5 | from client import * 6 | 7 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 8 | 9 | 10 | async def main(): 11 | wallet = import_wallet(mnemonics) 12 | 13 | _, _, _, hv_wallet = Wallets.from_mnemonics(version=WalletVersionEnum.hv2, mnemonics=mnemonics) 14 | state_init = hv_wallet.create_state_init()['state_init'] 15 | 16 | client = await get_client(7, False) 17 | 18 | query = wallet.create_transfer_message( 19 | to_addr=hv_wallet.address.to_string(), 20 | state_init=state_init, 21 | seqno=await get_seqno(client, wallet.address.to_string()), 22 | amount=0.5 * 10**9 23 | ) 24 | 25 | await client.raw_send_message(query['message'].to_boc(False)) 26 | 27 | await client.close() 28 | 29 | 30 | if __name__ == '__main__': 31 | asyncio.run(main()) 32 | -------------------------------------------------------------------------------- /lesson_8/get_processed.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from wallets import import_wallet 4 | from mnemoincs import mnemonics 5 | from client import * 6 | 7 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 8 | 9 | 10 | async def main(qid: int): 11 | _, _, _, hv_wallet = Wallets.from_mnemonics(version=WalletVersionEnum.hv2, mnemonics=mnemonics) 12 | 13 | client = await get_client(7, False) 14 | 15 | result = await run_get_method( 16 | client, address=hv_wallet.address.to_string(), 17 | method='processed?', 18 | stack=[["num", qid]] 19 | ) 20 | 21 | print(result) 22 | 23 | await client.close() 24 | 25 | #7241219937983791104 26 | if __name__ == '__main__': 27 | asyncio.run(main(7241220814157119488)) 28 | -------------------------------------------------------------------------------- /lesson_8/mnemoincs.py: -------------------------------------------------------------------------------- 1 | mnemonics = ['early', 'claw', 'echo', 'energy', 'erase', 'damp', 'expire', 'brush', 'scrub', 'ripple', 'skirt', 'beach', 'club', 'believe', 'firm', 'rely', 'head', 'neck', 'doll', 'punch', 'domain', 'phone', 'render', 'dad'] 2 | 3 | if __name__ == '__main__': 4 | pass -------------------------------------------------------------------------------- /lesson_8/parse_hashmap.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from wallets import import_wallet 4 | from mnemoincs import mnemonics 5 | from client import * 6 | 7 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 8 | 9 | 10 | def main(): 11 | _, _, _, hv_wallet = Wallets.from_mnemonics(version=WalletVersionEnum.hv2, mnemonics=mnemonics) 12 | 13 | recieps = [ 14 | { 15 | 'address': 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', 16 | 'payload': 'comment 1', 17 | 'send_mode': 3, 18 | 'amount': 0.0001*10**9 19 | }, 20 | { 21 | 'address': 'EQAhE3sLxHZpsyZ_HecMuwzvXHKLjYx4kEUehhOy2JmCcHCT', 22 | 'payload': 'comment 2', 23 | 'send_mode': 3, 24 | 'amount': 0.0001 * 10 ** 9 25 | } 26 | ] * 10 27 | 28 | query = hv_wallet.create_transfer_message( 29 | recipients_list=recieps, 30 | query_id=0 31 | ) 32 | 33 | print(query['query_id']) 34 | 35 | message = query['signing_message'] 36 | 37 | print(message) 38 | 39 | result = read_dict(message.refs[0], 16) 40 | 41 | print(result) 42 | 43 | print(result['0000000000000000'].refs[0]) 44 | 45 | 46 | if __name__ == '__main__': 47 | main() 48 | -------------------------------------------------------------------------------- /lesson_8/transfer.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from wallets import import_wallet 4 | from mnemoincs import mnemonics 5 | from client import * 6 | 7 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 8 | 9 | 10 | async def main(): 11 | _, _, _, hv_wallet = Wallets.from_mnemonics(version=WalletVersionEnum.hv2, mnemonics=mnemonics) 12 | 13 | recieps = [ 14 | { 15 | 'address': 'EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', 16 | 'payload': 'comment 1', 17 | 'send_mode': 3, 18 | 'amount': 0.0001*10**9 19 | }, 20 | { 21 | 'address': 'EQAhE3sLxHZpsyZ_HecMuwzvXHKLjYx4kEUehhOy2JmCcHCT', 22 | 'payload': 'comment 2', 23 | 'send_mode': 3, 24 | 'amount': 0.0001 * 10 ** 9 25 | } 26 | ] * 10 27 | 28 | client = await get_client(7, False) 29 | 30 | query = hv_wallet.create_transfer_message( 31 | recipients_list=recieps, 32 | query_id=0 33 | ) 34 | 35 | print(query['query_id']) 36 | 37 | await client.raw_send_message(query['message'].to_boc(False)) 38 | 39 | await client.close() 40 | 41 | # 7241219937983791104 42 | # 7241220814157119488 43 | if __name__ == '__main__': 44 | asyncio.run(main()) 45 | -------------------------------------------------------------------------------- /lesson_8/wallets.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import asyncio 3 | import requests 4 | 5 | 6 | from tonsdk.contract.wallet import Wallets, WalletVersionEnum 7 | from tonsdk.crypto import mnemonic_new 8 | from tonsdk.utils import to_nano 9 | 10 | from mnemoincs import mnemonics as my_mnemonics 11 | from pytonlib import TonlibClient 12 | 13 | 14 | def create_wallet(): 15 | mnemonics, pub_k, priv_k, wallet = Wallets.create(WalletVersionEnum.v3r2, workchain=0) 16 | 17 | return wallet 18 | 19 | 20 | def import_wallet(mnemoincs): 21 | mnemonics, pub_k, priv_k, wallet = Wallets.from_mnemonics(mnemonics=mnemoincs) 22 | 23 | return wallet 24 | 25 | 26 | async def get_client(): 27 | ton_config = requests.get('https://ton.org/global.config.json').json() 28 | 29 | keystore_dir = '/tmp/ton_keystore' 30 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 31 | 32 | # init TonlibClient 33 | client = TonlibClient(ls_index=2, # choose LiteServer index to connect 34 | config=ton_config, 35 | keystore=keystore_dir) 36 | 37 | # init tonlibjson 38 | await client.init() 39 | 40 | return client 41 | 42 | 43 | async def deploy_wallet(): 44 | wallet = import_wallet(my_mnemonics) 45 | 46 | boc = wallet.create_init_external_message()['message'].to_boc(False) 47 | 48 | client = await get_client() 49 | 50 | await client.raw_send_message(boc) 51 | 52 | await client.close() 53 | 54 | 55 | async def deploy_wallet_internal(): 56 | 57 | new_wallet = create_wallet() 58 | 59 | state_init = new_wallet.create_state_init()['state_init'] 60 | 61 | wallet = import_wallet(my_mnemonics) 62 | 63 | boc = wallet.create_transfer_message( 64 | to_addr=new_wallet.address.to_string(), 65 | amount=to_nano(0.05, 'ton'), 66 | seqno=1, 67 | state_init=state_init 68 | )['message'].to_boc(False) 69 | 70 | client = await get_client() 71 | 72 | await client.raw_send_message(boc) 73 | 74 | await client.close() 75 | 76 | 77 | if __name__ == '__main__': 78 | 79 | asyncio.run(deploy_wallet_internal()) 80 | -------------------------------------------------------------------------------- /lesson_9/README.md: -------------------------------------------------------------------------------- 1 | # LESSON 9 2 | [![](https://img.shields.io/badge/%F0%9F%92%8E-TON-grey)](https://ton.org) 3 | 4 | ### [YouTube link](https://www.youtube.com/watch?v=1pM3mPYZGtQ) 5 | 6 | ### Lesson material: 7 | * [tonsdk](https://github.com/tonfactory/tonsdk) 8 | * [pytonlib](https://github.com/toncenter/pytonlib) 9 | 10 | 11 | * [Multisig wallet code](https://github.com/ton-blockchain/multisig-contract) 12 | * [Multisig wallet from the lesson](https://tonscan.org/address/EQCOpgZNmHhDe4nuZY6aQh3sgqgwgTBtCL4kZPYTDTDlZY_Y) 13 | 14 | ### Official Docs - [link](https://docs.ton.org) 15 | 16 | ## Lesson themes 17 | - Multisig wallets creation 18 | - Off-chain and on-chain signatures 19 | - Reading contracts data 20 | 21 | ## Folder structure 22 | - `client.py` - useful funcs with TonLibClient 23 | - `get_multisig.py` - Get multisig wallet data 24 | - `offchain/` - offchain signatures 25 | - `onchain/` - onchain signatures 26 | -------------------------------------------------------------------------------- /lesson_9/client.py: -------------------------------------------------------------------------------- 1 | import bitarray 2 | from pytonlib import TonlibClient 3 | from tonsdk.boc import Cell 4 | 5 | import requests 6 | from pathlib import Path 7 | 8 | from tvm_valuetypes import deserialize_boc, parse_hashmap 9 | 10 | 11 | async def get_client(ls_index: int, testnet: bool) -> TonlibClient: 12 | if testnet: 13 | url = 'https://ton.org/testnet-global.config.json' 14 | else: 15 | url = 'https://ton.org/global-config.json' 16 | 17 | config = requests.get(url).json() 18 | 19 | keystore_dir = '/tmp/ton_keystore' 20 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 21 | 22 | client = TonlibClient(ls_index=ls_index, config=config, keystore=keystore_dir, tonlib_timeout=10) 23 | await client.init() 24 | 25 | return client 26 | 27 | 28 | async def get_seqno(client: TonlibClient, address: str) -> int: 29 | data = await client.raw_run_method(method='seqno', stack_data=[], address=address) 30 | return int(data['stack'][0][1], 16) 31 | 32 | 33 | async def run_get_method(client: TonlibClient, address: str, method: str, stack: list) -> list: 34 | response = await client.raw_run_method(method=method, address=address, 35 | stack_data=stack) 36 | 37 | if response['exit_code'] != 0: 38 | raise Exception(f'get method exit code is {response["exit_code"]}') 39 | 40 | stack = response['stack'] 41 | 42 | return stack 43 | 44 | 45 | def read_dict(dict_cell: Cell, key_len: int) -> dict: 46 | cell = deserialize_boc(dict_cell.to_boc()) 47 | 48 | result = {} 49 | 50 | parse_hashmap(cell, key_len, result, bitarray.bitarray('')) 51 | 52 | for i in result: 53 | result[i] = Cell.one_from_boc(result[i].serialize_boc()) 54 | 55 | return result 56 | -------------------------------------------------------------------------------- /lesson_9/get_multisig.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from client import * 4 | from tonsdk.boc import Cell 5 | from tonsdk.contract.wallet import MultiSigWallet 6 | import base64 7 | 8 | 9 | async def get_data(client: TonlibClient, address: str): 10 | 11 | data = (await client.raw_get_account_state(address))['data'] 12 | cell_data = Cell.one_from_boc(base64.b64decode(data)) 13 | slice = cell_data.begin_parse() 14 | 15 | wallet_id = slice.read_uint(32) 16 | n, k = slice.read_uint(8), slice.read_uint(8) 17 | slice.skip_bits(64) 18 | owners = read_dict(slice.load_dict(), 8) 19 | 20 | public_keys = [] 21 | for owner in owners.values(): 22 | public_keys.append(owner.begin_parse().read_bytes(32)) 23 | 24 | return { 25 | 'wallet_id': wallet_id, 26 | 'n': n, 27 | 'k': k, 28 | 'public_keys': public_keys 29 | } 30 | 31 | 32 | async def get_multisig(client: TonlibClient, address: str): 33 | data = await get_data(client, address) 34 | 35 | wallet = MultiSigWallet(public_keys=data['public_keys'], k=data['k'], wallet_id=data['wallet_id']) 36 | 37 | return wallet 38 | 39 | 40 | async def main(): 41 | client = await get_client(0, False) 42 | wallet = await get_multisig(client, 'EQCOpgZNmHhDe4nuZY6aQh3sgqgwgTBtCL4kZPYTDTDlZY_Y') 43 | print(wallet.address.to_string(True, True, True)) 44 | await client.close() 45 | 46 | if __name__ == '__main__': 47 | asyncio.run(main()) 48 | -------------------------------------------------------------------------------- /lesson_9/offchain/client.py: -------------------------------------------------------------------------------- 1 | import bitarray 2 | from pytonlib import TonlibClient 3 | from tonsdk.boc import Cell 4 | 5 | import requests 6 | from pathlib import Path 7 | 8 | from tvm_valuetypes import deserialize_boc, parse_hashmap 9 | 10 | 11 | async def get_client(ls_index: int, testnet: bool) -> TonlibClient: 12 | if testnet: 13 | url = 'https://ton.org/testnet-global.config.json' 14 | else: 15 | url = 'https://ton.org/global-config.json' 16 | 17 | config = requests.get(url).json() 18 | 19 | keystore_dir = '/tmp/ton_keystore' 20 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 21 | 22 | client = TonlibClient(ls_index=ls_index, config=config, keystore=keystore_dir, tonlib_timeout=10) 23 | await client.init() 24 | 25 | return client 26 | 27 | 28 | async def get_seqno(client: TonlibClient, address: str) -> int: 29 | data = await client.raw_run_method(method='seqno', stack_data=[], address=address) 30 | return int(data['stack'][0][1], 16) 31 | 32 | 33 | async def run_get_method(client: TonlibClient, address: str, method: str, stack: list) -> list: 34 | response = await client.raw_run_method(method=method, address=address, 35 | stack_data=stack) 36 | 37 | if response['exit_code'] != 0: 38 | raise Exception(f'get method exit code is {response["exit_code"]}') 39 | 40 | stack = response['stack'] 41 | 42 | return stack 43 | 44 | 45 | def read_dict(dict_cell: Cell, key_len: int) -> dict: 46 | cell = deserialize_boc(dict_cell.to_boc()) 47 | 48 | result = {} 49 | 50 | parse_hashmap(cell, key_len, result, bitarray.bitarray('')) 51 | 52 | for i in result: 53 | result[i] = Cell.one_from_boc(result[i].serialize_boc()) 54 | 55 | return result 56 | -------------------------------------------------------------------------------- /lesson_9/offchain/general.py: -------------------------------------------------------------------------------- 1 | from tonsdk.contract.wallet import MultiSigWallet, MultiSigOrder, MultiSigOrderBuilder 2 | from tonsdk.crypto import mnemonic_new, mnemonic_to_wallet_key 3 | from tonsdk.utils import Address, bytes_to_b64str, b64str_to_bytes, to_nano 4 | 5 | 6 | """import or generate mnemonics""" 7 | # mnemonics1 = mnemonic_new() 8 | # mnemonics2 = mnemonic_new() 9 | # mnemonics3 = mnemonic_new() 10 | mnemonics1 = ['broken', 'decade', 'unit', 'bird', 'enrich', 'great', 'nurse', 'offer', 'rescue', 'sound', 'pole', 'true', 'dignity', 'buyer', 'provide', 'boil', 'connect', 'universe', 'model', 'add', 'obtain', 'hire', 'gift', 'swim'] 11 | mnemonics2 = ['rather', 'voice', 'zone', 'fold', 'rotate', 'crane', 'roast', 'brave', 'motor', 'kid', 'note', 'squirrel', 'piece', 'home', 'expose', 'bench', 'flame', 'wood', 'person', 'assist', 'vocal', 'bomb', 'dismiss', 'diesel'] 12 | mnemonics3 = ['author', 'holiday', 'figure', 'luxury', 'leg', 'fringe', 'sibling', 'citizen', 'enforce', 'convince', 'silly', 'girl', 'remove', 'purity', 'sand', 'paper', 'file', 'review', 'window', 'kite', 'illegal', 'allow', 'satisfy', 'wait'] 13 | 14 | 15 | """get pub and priv keys from mnemonics""" 16 | pub_k0, priv_k0 = mnemonic_to_wallet_key(mnemonics1) 17 | pub_k1, priv_k1 = mnemonic_to_wallet_key(mnemonics2) 18 | pub_k2, priv_k2 = mnemonic_to_wallet_key(mnemonics3) 19 | 20 | 21 | wallet = MultiSigWallet(public_keys=[pub_k0, pub_k1, pub_k2], k=2, wallet_id=0) 22 | 23 | 24 | """create order and add message""" 25 | order1 = MultiSigOrderBuilder(wallet.options["wallet_id"]) 26 | order1.add_message(to_addr='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', amount=to_nano('0.01', 'ton'), send_mode=3, payload='hello from python tonsdk') 27 | 28 | 29 | """build and sign order with all required priv keys""" 30 | order1b = order1.build() 31 | order1b.sign(0, priv_k0) 32 | order1b.sign(1, priv_k1) 33 | 34 | query = wallet.create_transfer_message(order1b, priv_k1) 35 | transfer_boc = bytes_to_b64str(query["message"].to_boc(False)) 36 | print('Base64boc to transfer tons: ', transfer_boc) -------------------------------------------------------------------------------- /lesson_9/offchain/get_multisig.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from client import * 4 | from tonsdk.boc import Cell 5 | from tonsdk.contract.wallet import MultiSigWallet 6 | import base64 7 | 8 | 9 | async def get_data(client: TonlibClient, address: str): 10 | 11 | data = (await client.raw_get_account_state(address))['data'] 12 | cell_data = Cell.one_from_boc(base64.b64decode(data)) 13 | slice = cell_data.begin_parse() 14 | 15 | wallet_id = slice.read_uint(32) 16 | n, k = slice.read_uint(8), slice.read_uint(8) 17 | slice.skip_bits(64) 18 | owners = read_dict(slice.load_dict(), 8) 19 | 20 | public_keys = [] 21 | for owner in owners.values(): 22 | public_keys.append(owner.begin_parse().read_bytes(32)) 23 | 24 | return { 25 | 'wallet_id': wallet_id, 26 | 'n': n, 27 | 'k': k, 28 | 'public_keys': public_keys 29 | } 30 | 31 | 32 | async def get_multisig(client: TonlibClient, address: str): 33 | data = await get_data(client, address) 34 | 35 | wallet = MultiSigWallet(public_keys=data['public_keys'], k=data['k'], wallet_id=data['wallet_id']) 36 | 37 | return wallet 38 | 39 | 40 | async def main(): 41 | client = await get_client(0, False) 42 | wallet = await get_multisig(client, 'EQCOpgZNmHhDe4nuZY6aQh3sgqgwgTBtCL4kZPYTDTDlZY_Y') 43 | print(wallet.address.to_string(True, True, True)) 44 | await client.close() 45 | 46 | if __name__ == '__main__': 47 | asyncio.run(main()) 48 | -------------------------------------------------------------------------------- /lesson_9/offchain/main.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | 4 | from tonsdk.contract.wallet import MultiSigWallet, MultiSigOrder, MultiSigOrderBuilder 5 | from tonsdk.crypto import mnemonic_new, mnemonic_to_wallet_key 6 | from tonsdk.utils import Address, bytes_to_b64str, b64str_to_bytes, to_nano 7 | from client import * 8 | from get_multisig import get_multisig 9 | from owner_1 import sign 10 | 11 | 12 | mnemonics0 = ['broken', 'decade', 'unit', 'bird', 'enrich', 'great', 'nurse', 'offer', 'rescue', 'sound', 'pole', 'true', 'dignity', 'buyer', 'provide', 'boil', 'connect', 'universe', 'model', 'add', 'obtain', 'hire', 'gift', 'swim'] 13 | pub_k0, priv_k0 = mnemonic_to_wallet_key(mnemonics0) 14 | 15 | 16 | async def main(): 17 | client = await get_client(0, False) 18 | 19 | wallet = await get_multisig(client, 'EQCOpgZNmHhDe4nuZY6aQh3sgqgwgTBtCL4kZPYTDTDlZY_Y') 20 | 21 | order1 = MultiSigOrderBuilder(wallet.options["wallet_id"]) 22 | order1.add_message(to_addr='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', amount=to_nano('0.01', 'ton'), send_mode=3, payload='hello from pyton lessons eng') 23 | order1b = order1.build() 24 | # order1b.sign(0, priv_k0) 25 | 26 | signature = sign(base64.b64encode(order1b.payload.to_boc()).decode()) 27 | 28 | order1b.add_signature(1, base64.b64decode(signature), wallet) 29 | 30 | query = wallet.create_transfer_message(order1b, priv_k0) 31 | transfer_boc = query["message"].to_boc(False) 32 | await client.raw_send_message(transfer_boc) 33 | 34 | await client.close() 35 | 36 | 37 | if __name__ == '__main__': 38 | asyncio.run(main()) 39 | -------------------------------------------------------------------------------- /lesson_9/offchain/owner_1.py: -------------------------------------------------------------------------------- 1 | from tonsdk.boc import Cell 2 | from tonsdk.utils import sign_message 3 | import base64 4 | from tonsdk.crypto import mnemonic_new, mnemonic_to_wallet_key 5 | 6 | 7 | mnemonics1 = ['rather', 'voice', 'zone', 'fold', 'rotate', 'crane', 'roast', 'brave', 'motor', 'kid', 'note', 'squirrel', 'piece', 'home', 'expose', 'bench', 'flame', 'wood', 'person', 'assist', 'vocal', 'bomb', 'dismiss', 'diesel'] 8 | pub_k1, priv_k1 = mnemonic_to_wallet_key(mnemonics1) 9 | 10 | 11 | def sign(b64str_boc: str): 12 | cell = Cell.one_from_boc(base64.b64decode(b64str_boc)) 13 | 14 | """ 15 | validate message 16 | """ 17 | slice = cell.begin_parse() 18 | 19 | return base64.b64encode(sign_message(cell.bytes_hash(), priv_k1).signature) 20 | -------------------------------------------------------------------------------- /lesson_9/onchain/client.py: -------------------------------------------------------------------------------- 1 | import bitarray 2 | from pytonlib import TonlibClient 3 | from tonsdk.boc import Cell 4 | 5 | import requests 6 | from pathlib import Path 7 | 8 | from tvm_valuetypes import deserialize_boc, parse_hashmap 9 | 10 | 11 | async def get_client(ls_index: int, testnet: bool) -> TonlibClient: 12 | if testnet: 13 | url = 'https://ton.org/testnet-global.config.json' 14 | else: 15 | url = 'https://ton.org/global-config.json' 16 | 17 | config = requests.get(url).json() 18 | 19 | keystore_dir = '/tmp/ton_keystore' 20 | Path(keystore_dir).mkdir(parents=True, exist_ok=True) 21 | 22 | client = TonlibClient(ls_index=ls_index, config=config, keystore=keystore_dir, tonlib_timeout=10) 23 | await client.init() 24 | 25 | return client 26 | 27 | 28 | async def get_seqno(client: TonlibClient, address: str) -> int: 29 | data = await client.raw_run_method(method='seqno', stack_data=[], address=address) 30 | return int(data['stack'][0][1], 16) 31 | 32 | 33 | async def run_get_method(client: TonlibClient, address: str, method: str, stack: list) -> list: 34 | response = await client.raw_run_method(method=method, address=address, 35 | stack_data=stack) 36 | 37 | if response['exit_code'] != 0: 38 | raise Exception(f'get method exit code is {response["exit_code"]}') 39 | 40 | stack = response['stack'] 41 | 42 | return stack 43 | 44 | 45 | def read_dict(dict_cell: Cell, key_len: int) -> dict: 46 | cell = deserialize_boc(dict_cell.to_boc()) 47 | 48 | result = {} 49 | 50 | parse_hashmap(cell, key_len, result, bitarray.bitarray('')) 51 | 52 | for i in result: 53 | result[i] = Cell.one_from_boc(result[i].serialize_boc()) 54 | 55 | return result 56 | -------------------------------------------------------------------------------- /lesson_9/onchain/general.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from tonsdk.contract.wallet import MultiSigWallet, MultiSigOrder, MultiSigOrderBuilder 4 | from tonsdk.crypto import mnemonic_new, mnemonic_to_wallet_key, verify_sign 5 | from tonsdk.utils import Address, bytes_to_b64str, b64str_to_bytes, to_nano, sign_message 6 | 7 | from client import * 8 | 9 | 10 | """import or generate mnemonics""" 11 | # mnemonics1 = mnemonic_new() 12 | # mnemonics2 = mnemonic_new() 13 | # mnemonics3 = mnemonic_new() 14 | mnemonics1 = ['broken', 'decade', 'unit', 'bird', 'enrich', 'great', 'nurse', 'offer', 'rescue', 'sound', 'pole', 'true', 'dignity', 'buyer', 'provide', 'boil', 'connect', 'universe', 'model', 'add', 'obtain', 'hire', 'gift', 'swim'] 15 | mnemonics2 = ['rather', 'voice', 'zone', 'fold', 'rotate', 'crane', 'roast', 'brave', 'motor', 'kid', 'note', 'squirrel', 'piece', 'home', 'expose', 'bench', 'flame', 'wood', 'person', 'assist', 'vocal', 'bomb', 'dismiss', 'diesel'] 16 | mnemonics3 = ['author', 'holiday', 'figure', 'luxury', 'leg', 'fringe', 'sibling', 'citizen', 'enforce', 'convince', 'silly', 'girl', 'remove', 'purity', 'sand', 'paper', 'file', 'review', 'window', 'kite', 'illegal', 'allow', 'satisfy', 'wait'] 17 | 18 | 19 | """get pub and priv keys from mnemonics""" 20 | pub_k0, priv_k0 = mnemonic_to_wallet_key(mnemonics1) 21 | pub_k1, priv_k1 = mnemonic_to_wallet_key(mnemonics2) 22 | pub_k2, priv_k2 = mnemonic_to_wallet_key(mnemonics3) 23 | 24 | 25 | wallet = MultiSigWallet(public_keys=[pub_k0, pub_k1, pub_k2], k=2, wallet_id=0) 26 | 27 | 28 | """create order and add message""" 29 | order1 = MultiSigOrderBuilder(wallet.options["wallet_id"]) 30 | message = order1.add_message(to_addr='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', amount=to_nano('0.01', 'ton'), send_mode=3, payload='hello from python tonsdk') 31 | query_id = order1.query_id # remember this query id or share this with other owners 32 | 33 | 34 | """build and sign order""" 35 | order1b = order1.build() 36 | order1b.sign(0, priv_k0) 37 | 38 | 39 | """send boc to blockchain""" 40 | query = wallet.create_transfer_message(order1b, priv_k0) 41 | transfer_boc = query["message"].to_boc(False) 42 | 43 | 44 | """wait for transaction processing""" 45 | 46 | 47 | """create second order with query id from first step""" 48 | order2 = MultiSigOrderBuilder(wallet.options["wallet_id"], query_id=query_id) 49 | 50 | 51 | """add the same message here""" 52 | order2.add_message(to_addr='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', amount=to_nano('0.01', 'ton'), 53 | send_mode=3, payload='hello from python tonsdk') 54 | 55 | """build and sign order""" 56 | order2b = order2.build() 57 | order2b.sign(1, priv_k1) 58 | 59 | 60 | """send boc to blockchain""" 61 | query_2 = wallet.create_transfer_message(order2b, priv_k1) 62 | transfer_boc_2 = query_2["message"].to_boc(False) 63 | 64 | 65 | async def main(): 66 | client = await get_client(0, False) 67 | 68 | await client.raw_send_message(transfer_boc) 69 | 70 | await asyncio.sleep(20) 71 | 72 | await client.raw_send_message(transfer_boc_2) 73 | 74 | 75 | if __name__ == '__main__': 76 | asyncio.run(main()) 77 | -------------------------------------------------------------------------------- /lesson_9/onchain/get_multisig.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from client import * 4 | from tonsdk.boc import Cell 5 | from tonsdk.contract.wallet import MultiSigWallet 6 | import base64 7 | 8 | 9 | async def get_data(client: TonlibClient, address: str): 10 | 11 | data = (await client.raw_get_account_state(address))['data'] 12 | cell_data = Cell.one_from_boc(base64.b64decode(data)) 13 | slice = cell_data.begin_parse() 14 | 15 | wallet_id = slice.read_uint(32) 16 | n, k = slice.read_uint(8), slice.read_uint(8) 17 | slice.skip_bits(64) 18 | owners = read_dict(slice.load_dict(), 8) 19 | 20 | public_keys = [] 21 | for owner in owners.values(): 22 | public_keys.append(owner.begin_parse().read_bytes(32)) 23 | 24 | return { 25 | 'wallet_id': wallet_id, 26 | 'n': n, 27 | 'k': k, 28 | 'public_keys': public_keys 29 | } 30 | 31 | 32 | async def get_multisig(client: TonlibClient, address: str): 33 | data = await get_data(client, address) 34 | 35 | wallet = MultiSigWallet(public_keys=data['public_keys'], k=data['k'], wallet_id=data['wallet_id']) 36 | 37 | return wallet 38 | 39 | 40 | async def main(): 41 | client = await get_client(0, False) 42 | wallet = await get_multisig(client, 'EQCOpgZNmHhDe4nuZY6aQh3sgqgwgTBtCL4kZPYTDTDlZY_Y') 43 | print(wallet.address.to_string(True, True, True)) 44 | await client.close() 45 | 46 | if __name__ == '__main__': 47 | asyncio.run(main()) 48 | -------------------------------------------------------------------------------- /lesson_9/onchain/owner_0.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import base64 3 | 4 | from tonsdk.contract.wallet import MultiSigWallet, MultiSigOrder, MultiSigOrderBuilder 5 | from tonsdk.crypto import mnemonic_new, mnemonic_to_wallet_key 6 | from tonsdk.utils import Address, bytes_to_b64str, b64str_to_bytes, to_nano 7 | from client import * 8 | from get_multisig import get_multisig 9 | from owner_1 import sign 10 | 11 | mnemonics0 = ['broken', 'decade', 'unit', 'bird', 'enrich', 'great', 'nurse', 'offer', 'rescue', 'sound', 'pole', 12 | 'true', 'dignity', 'buyer', 'provide', 'boil', 'connect', 'universe', 'model', 'add', 'obtain', 'hire', 13 | 'gift', 'swim'] 14 | 15 | pub_k0, priv_k0 = mnemonic_to_wallet_key(mnemonics0) 16 | 17 | 18 | async def main(): 19 | client = await get_client(0, False) 20 | 21 | wallet = await get_multisig(client, 'EQCOpgZNmHhDe4nuZY6aQh3sgqgwgTBtCL4kZPYTDTDlZY_Y') 22 | 23 | order1 = MultiSigOrderBuilder(wallet.options["wallet_id"]) 24 | order1.add_message(to_addr='EQCD39VS5jcptHL8vMjEXrzGaRcCVYto7HUn4bpAOg8xqB2N', amount=to_nano('0.001', 'ton'), 25 | send_mode=3, payload='hello from pyton lessons eng') 26 | order1b = order1.build() 27 | 28 | query = wallet.create_transfer_message(order1b, priv_k0) 29 | transfer_boc = query["message"].to_boc(False) 30 | await client.raw_send_message(transfer_boc) 31 | 32 | await client.close() 33 | 34 | 35 | if __name__ == '__main__': 36 | asyncio.run(main()) 37 | -------------------------------------------------------------------------------- /lesson_9/onchain/owner_1.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from tonsdk.contract.wallet import MultiSigWallet, MultiSigOrder, MultiSigOrderBuilder 3 | 4 | from tonsdk.boc import Cell 5 | from tonsdk.utils import sign_message 6 | import base64 7 | from tonsdk.crypto import mnemonic_new, mnemonic_to_wallet_key 8 | from get_multisig import get_multisig 9 | from client import * 10 | 11 | 12 | 13 | mnemonics1 = ['rather', 'voice', 'zone', 'fold', 'rotate', 'crane', 'roast', 'brave', 'motor', 'kid', 'note', 'squirrel', 'piece', 'home', 'expose', 'bench', 'flame', 'wood', 'person', 'assist', 'vocal', 'bomb', 'dismiss', 'diesel'] 14 | pub_k1, priv_k1 = mnemonic_to_wallet_key(mnemonics1) 15 | 16 | 17 | async def get_messages_unsigned_by_id(client: TonlibClient, owner_id): 18 | stack = await run_get_method(client, address='EQCOpgZNmHhDe4nuZY6aQh3sgqgwgTBtCL4kZPYTDTDlZY_Y', 19 | method='get_messages_unsigned_by_id', stack=[["num", owner_id]]) 20 | cell_dict = Cell.one_from_boc(base64.b64decode(stack[0][1]['bytes'])) 21 | hashmap = read_dict(cell_dict, 64) 22 | return hashmap 23 | 24 | 25 | async def main(): 26 | client = await get_client(0, False) 27 | hashmap = await get_messages_unsigned_by_id(client, 1) 28 | wallet = await get_multisig(client, 'EQCOpgZNmHhDe4nuZY6aQh3sgqgwgTBtCL4kZPYTDTDlZY_Y') 29 | 30 | for query_id, order in hashmap.items(): 31 | order2 = MultiSigOrderBuilder(wallet.options["wallet_id"], query_id=int(query_id, 2)) 32 | order2.add_message_from_cell(order.refs[0], 3) 33 | 34 | """ 35 | validate message 36 | """ 37 | 38 | order2b = order2.build() 39 | order2b.sign(1, priv_k1) 40 | query = wallet.create_transfer_message(order2b, priv_k1) 41 | transfer_boc = query["message"].to_boc(False) 42 | await client.raw_send_message(transfer_boc) 43 | 44 | await client.close() 45 | 46 | 47 | if __name__ == '__main__': 48 | asyncio.run(main()) 49 | --------------------------------------------------------------------------------